Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9faa907
fix: 책 저장 페이지 design 수정
heeeeyong Sep 1, 2025
8aef1d6
Merge pull request #228 from THIP-TextHip/main
ho0010 Sep 1, 2025
5cd1df0
refactor: 유저 프로필 부분 Hover 제거 및 칭호 색상 매핑
ljh130334 Sep 2, 2025
f18efc4
feat: 모임방 독서메이트 Hover 제거
ljh130334 Sep 2, 2025
f7933a8
refactor: 유저 프로필 부분 border 추가
ljh130334 Sep 2, 2025
0c37602
fix: 하드코딩 된 토큰 주석처리
ljh130334 Sep 2, 2025
bba8dc8
Merge pull request #230 from THIP-TextHip/qa/jihyeon
ljh130334 Sep 2, 2025
1dd76b2
feat: 오늘의 한마디 삭제 API 연결
ljh130334 Sep 2, 2025
58a6077
fix: 하드코딩된 토큰 주석처리
ljh130334 Sep 2, 2025
d4eaa41
fix: 사용자 찾기 프로필이미지 안보이는 문제 수정
heeeeyong Sep 3, 2025
f5b01df
fix: 빌드에러 수정
heeeeyong Sep 3, 2025
81b5d52
fix: 헤더 임시 엑세스토큰 제거
heeeeyong Sep 3, 2025
3dd456d
Merge branch 'develop' into chore/minor-updates
heeeeyong Sep 3, 2025
c8c7851
fix: 변수명 수정
heeeeyong Sep 3, 2025
3f5f3ed
fix: 책 검색 레이아웃
ho0010 Sep 3, 2025
7c12d98
fix: 책 상세 페이지 하단 여백 발생
ho0010 Sep 3, 2025
cae0452
Merge pull request #234 from THIP-TextHip/fix/QA9-1
ho0010 Sep 3, 2025
1fcb71f
Merge branch 'develop' of https://github.com/THIP-TextHip/THIP-Web in…
heeeeyong Sep 3, 2025
596494b
fix: 투표 버튼 클릭 시 더보기 열리는 문제 해결 및 블라인드된 기록 클릭 문제 해결
ljh130334 Sep 6, 2025
5bd68a3
refactor: 페이지 입력 부분 길이 늘리기
ljh130334 Sep 7, 2025
e0710f2
fix: 토큰 주석 처리
ljh130334 Sep 7, 2025
b151dd6
Merge pull request #232 from THIP-TextHip/feat/api-today
ljh130334 Sep 8, 2025
03812ff
Merge pull request #235 from THIP-TextHip/qa/jihyeon
ljh130334 Sep 8, 2025
26c6173
feat: 방나가기 API 연동
ljh130334 Sep 9, 2025
1c27ed1
feat: 기록 수정 API 연동
ljh130334 Sep 9, 2025
d05d8ca
feat: 투표 수정 API 연동 및 수정 페이지에서 입력 커서 마지막에 위치하도록 수정
ljh130334 Sep 9, 2025
77af4b7
feat: presigned URL API 연동 및 이미지 업로드 방식 수정
ljh130334 Sep 9, 2025
5ba4424
refactor: console.log 제거
ljh130334 Sep 9, 2025
126ca23
Merge pull request #236 from THIP-TextHip/feat/api-rooms-jh
ljh130334 Sep 9, 2025
3300b4e
Merge pull request #237 from THIP-TextHip/hotfix/image
ljh130334 Sep 9, 2025
73935a6
fix: 모바일 모달 가림현상 제거
heeeeyong Sep 10, 2025
54f0955
design: button-radius 값 수정
heeeeyong Sep 10, 2025
8881134
Merge pull request #233 from THIP-TextHip/chore/minor-updates
heeeeyong Sep 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 82 additions & 44 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
overscroll-behavior: none;
touch-action: pan-y pinch-zoom;
}

* {
overscroll-behavior-x: none;
}
Expand All @@ -15,50 +15,78 @@
// 좌우 스와이프 네비게이션 완전 차단
let startX = null;
let startY = null;

document.addEventListener('touchstart', function(e) {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}, { passive: false });

document.addEventListener('touchmove', function(e) {
if (startX === null || startY === null) return;

let currentX = e.touches[0].clientX;
let currentY = e.touches[0].clientY;
let diffX = Math.abs(currentX - startX);
let diffY = Math.abs(currentY - startY);

// 수평 스와이프 감지시 무조건 차단
if (diffX > 5 && diffX > diffY) {
e.preventDefault();
e.stopPropagation();
return false;
}
}, { passive: false, capture: true });

document.addEventListener('touchend', function(e) {
startX = null;
startY = null;
}, { passive: false });

document.addEventListener('touchcancel', function(e) {
startX = null;
startY = null;
}, { passive: false });


document.addEventListener(
'touchstart',
function (e) {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
},
{ passive: false },
);

document.addEventListener(
'touchmove',
function (e) {
if (startX === null || startY === null) return;

let currentX = e.touches[0].clientX;
let currentY = e.touches[0].clientY;
let diffX = Math.abs(currentX - startX);
let diffY = Math.abs(currentY - startY);

// 수평 스와이프 감지시 무조건 차단
if (diffX > 5 && diffX > diffY) {
e.preventDefault();
e.stopPropagation();
return false;
}
},
{ passive: false, capture: true },
);

document.addEventListener(
'touchend',
function (e) {
startX = null;
startY = null;
},
{ passive: false },
);

document.addEventListener(
'touchcancel',
function (e) {
startX = null;
startY = null;
},
{ passive: false },
);

// 추가적인 제스처 차단
document.addEventListener('gesturestart', function(e) {
e.preventDefault();
}, { passive: false });

document.addEventListener('gesturechange', function(e) {
e.preventDefault();
}, { passive: false });

document.addEventListener('gestureend', function(e) {
e.preventDefault();
}, { passive: false });
document.addEventListener(
'gesturestart',
function (e) {
e.preventDefault();
},
{ passive: false },
);

document.addEventListener(
'gesturechange',
function (e) {
e.preventDefault();
},
{ passive: false },
);

document.addEventListener(
'gestureend',
function (e) {
e.preventDefault();
},
{ passive: false },
);
</script>
<script>
(function (m, a, z, e) {
Expand Down Expand Up @@ -86,6 +114,15 @@
'77efe927-4d36-4c1a-9e5f-b47c9b90fff9',
);
</script>
<script>
function applyVv() {
const vv = window.visualViewport;
const h = vv ? vv.height : window.innerHeight;
document.documentElement.style.setProperty('--vvh', `${h}px`);
}
applyVv();
(window.visualViewport || window).addEventListener('resize', applyVv);
</script>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/assets/custom_favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand All @@ -94,6 +131,7 @@
<meta property="og:description" content="커뮤니티형 독서 기록 플랫폼. 띱. THIP." />
<meta property="og:image" content="https://thip.co.kr/assets/thumbnail.jpeg" />
<meta property="og:url" content="https://thip.co.kr" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
</head>
<body>
<div id="root"></div>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"lint": "eslint .",
"preview": "vite preview",
"format": "prettier . --write",
"format:check": "prettier . --check"
"format:check": "prettier . --check",
"dev:lan": "vite --host 0.0.0.0 --port 5173"
},
"dependencies": {
"@emotion/css": "^11.13.5",
Expand Down
22 changes: 4 additions & 18 deletions src/api/feeds/createFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface CreateFeedBody {
contentBody: string;
isPublic: boolean;
tagList?: string[];
imageUrls?: string[];
}

/** 성공 응답 */
Expand All @@ -29,27 +30,12 @@ export type CreateFeedResponse = CreateFeedSuccess | CreateFeedFail;

/**
* 피드 작성 API
* - multipart/form-data
* - request: application/json (Blob로 감싸 전송)
* - images: File[] (선택값, 없으면 미첨부)
* - application/json (presigned URL 방식)
* - imageUrls: 미리 S3에 업로드한 이미지의 CloudFront URL 목록
*/
export const createFeed = async (
body: CreateFeedBody,
images?: File[],
): Promise<CreateFeedResponse> => {
const form = new FormData();

// request 파트(JSON) - 필수
form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' }));

// images 파트들 - 선택
if (images && images.length > 0) {
images.forEach(file => form.append('images', file));
}

const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, {
headers: { 'Content-Type': 'multipart/form-data' },
});

const { data } = await apiClient.post<CreateFeedResponse>('/feeds', body);
return data;
};
29 changes: 29 additions & 0 deletions src/api/feeds/getPresignedUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { apiClient } from '../index';

export interface PresignedUrlRequest {
extension: string;
size: number;
}

export interface PresignedUrlResponse {
isSuccess: boolean;
code: number;
message: string;
data?: {
presignedUrls: Array<{
presignedUrl: string;
fileUrl: string;
}>;
};
}

export const getPresignedUrl = async (
requests: PresignedUrlRequest[]
): Promise<PresignedUrlResponse> => {
const { data } = await apiClient.post<PresignedUrlResponse>(
'/feeds/images/presigned-url',
requests
);

return data;
};
19 changes: 19 additions & 0 deletions src/api/feeds/uploadToS3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const uploadFileToS3 = async (
presignedUrl: string,
file: File
): Promise<boolean> => {
try {
const response = await fetch(presignedUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});

return response.ok;
} catch (error) {
console.error('S3 업로드 실패:', error);
return false;
}
};
Comment on lines +1 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

대외 호출: 타임아웃/재시도/에러 메시지 보강 필요

현재 무한 대기 가능성과 일시적 5xx에 취약합니다. 업로드 UX/복구력을 위해 최소 타임아웃과 제한적 재시도를 권장합니다.

 export const uploadFileToS3 = async (
   presignedUrl: string,
   file: File
 ): Promise<boolean> => {
   try {
-    const response = await fetch(presignedUrl, {
-      method: 'PUT',
-      body: file,
-      headers: {
-        'Content-Type': file.type,
-      },
-    });
+    // 15s 타임아웃 + 최대 2회 재시도(총 3회)
+    const maxAttempts = 3;
+    const timeoutMs = 15000;
+    let lastError: unknown = null;
+
+    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+      const controller = new AbortController();
+      const timer = setTimeout(() => controller.abort(), timeoutMs);
+      try {
+        const response = await fetch(presignedUrl, {
+          method: 'PUT',
+          body: file,
+          headers: { 'Content-Type': file.type },
+          signal: controller.signal,
+        });
+        clearTimeout(timer);
+        if (response.ok) return true;
+        // 5xx만 재시도, 그 외는 즉시 실패
+        if (response.status < 500 || response.status >= 600) {
+          const msg = await response.text().catch(() => '');
+          console.error(`S3 업로드 실패: ${response.status} ${msg}`); 
+          return false;
+        }
+      } catch (e) {
+        clearTimeout(timer);
+        lastError = e;
+        // AbortError 등은 재시도
+      }
+      // 지수 백오프(최대 ~1s)
+      await new Promise(r => setTimeout(r, Math.min(1000, 100 * 2 ** (attempt - 1))));
+    }
+    console.error('S3 업로드 반복 실패:', lastError);
+    return false;
-    return response.ok;
   } catch (error) {
     console.error('S3 업로드 실패:', error);
     return false;
   }
 };
🤖 Prompt for AI Agents
In src/api/feeds/uploadToS3.ts around lines 1 to 19, the current uploadFileToS3
can hang indefinitely and has no retry for transient 5xx errors; add an
AbortController-based timeout (e.g., configurable ms) to ensure fetch cancels
after a max wait, implement a small bounded retry loop (e.g., 2-3 attempts) with
exponential backoff for network errors and 5xx responses only, and surface
richer error logs (include attempt number, http status, and response text or
error message) before returning false so callers can observe failure reasons.

5 changes: 0 additions & 5 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export const apiClient = axios.create({
headers: {
'Content-Type': 'application/json',
},
withCredentials: true, // 쿠키 자동 전송 설정
});
// Request 인터셉터: localStorage의 토큰을 헤더에 자동 추가
apiClient.interceptors.request.use(
Expand All @@ -19,13 +18,9 @@ apiClient.interceptors.request.use(
const authToken = localStorage.getItem('authToken');

if (authToken) {
// 토큰이 있으면 Authorization 헤더에 추가
console.log('🔑 Authorization 헤더에 토큰 추가');
config.headers.Authorization = `Bearer ${authToken}`;
} else {
console.log('❌ localStorage에 토큰이 없습니다.');
// config.headers.Authorization =
// 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.BSGuoMWlrzc0oKgSJXHEycxdzzY9-e7gD4xh-wSDemc';
}

return config;
Expand Down
44 changes: 44 additions & 0 deletions src/api/record/updateRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { apiClient } from '../index';
import type { UpdateRecordRequest, UpdateRecordData, ApiResponse } from '@/types/record';

// API 응답 타입
export type UpdateRecordResponse = ApiResponse<UpdateRecordData>;

// 기록 수정 API 함수
export const updateRecord = async (
roomId: number,
recordId: number,
recordData: UpdateRecordRequest,
): Promise<UpdateRecordResponse> => {
try {
const response = await apiClient.patch<UpdateRecordResponse>(
`/rooms/${roomId}/records/${recordId}`,
recordData,
);
return response.data;
} catch (error) {
console.error('기록 수정 API 오류:', error);
throw error;
}
};

/*
사용 예시:
const recordData: UpdateRecordRequest = {
content: "수정된 기록 내용입니다~~"
};

try {
const result = await updateRecord(1, 123, recordData);
if (result.isSuccess) {
console.log("수정된 방 ID:", result.data.roomId);
// 성공 처리 로직
} else {
console.error("기록 수정 실패:", result.message);
// 실패 처리 로직
}
} catch (error) {
console.error("API 호출 오류:", error);
// 에러 처리 로직
}
*/
44 changes: 44 additions & 0 deletions src/api/record/updateVote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { apiClient } from '../index';
import type { UpdateVoteRequest, UpdateVoteData, ApiResponse } from '@/types/record';

// API 응답 타입
export type UpdateVoteResponse = ApiResponse<UpdateVoteData>;

// 투표 수정 API 함수
export const updateVote = async (
roomId: number,
voteId: number,
voteData: UpdateVoteRequest,
): Promise<UpdateVoteResponse> => {
try {
const response = await apiClient.patch<UpdateVoteResponse>(
`/rooms/${roomId}/votes/${voteId}`,
voteData,
);
return response.data;
} catch (error) {
console.error('투표 수정 API 오류:', error);
throw error;
}
};

/*
사용 예시:
const voteData: UpdateVoteRequest = {
content: "수정된 투표 내용입니다~"
};

try {
const result = await updateVote(1, 123, voteData);
if (result.isSuccess) {
console.log("수정된 방 ID:", result.data.roomId);
// 성공 처리 로직
} else {
console.error("투표 수정 실패:", result.message);
// 실패 처리 로직
}
} catch (error) {
console.error("API 호출 오류:", error);
// 에러 처리 로직
}
*/
28 changes: 28 additions & 0 deletions src/api/rooms/deleteDailyGreeting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { apiClient } from '../index';

// 오늘의 한마디 삭제 응답 타입
export interface DeleteDailyGreetingResponse {
isSuccess: boolean;
code: number;
message: string;
data: {
roomId: number;
};
}

// 오늘의 한마디 삭제 API 함수
export const deleteDailyGreeting = async (
roomId: number,
attendanceCheckId: number,
): Promise<DeleteDailyGreetingResponse> => {
try {
const response = await apiClient.delete<DeleteDailyGreetingResponse>(
`/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
);

return response.data;
} catch (error) {
Comment on lines +18 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

DELETE 204(No Content) 응답 시 타입-런타임 불일치로 실패 처리될 위험

서버가 204를 반환하면 response.data가 비거나 없음으로 내려올 수 있어, 호출부에서 result.isSuccess 검사 시 실패로 오인됩니다. 바디에 의존하지 말고 HTTP status로 성공을 판정하도록 정규화하세요.

한 가지 완화안(호출부 변경 없이 204 허용):

-    const response = await apiClient.delete<DeleteDailyGreetingResponse>(
-      `/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
-    );
-
-    return response.data;
+    const response = await apiClient.delete(
+      `/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
+    );
+    // 2xx는 성공으로 정규화
+    if (response.status >= 200 && response.status < 300) {
+      return (response.data as DeleteDailyGreetingResponse) ?? {
+        isSuccess: true,
+        code: response.status,
+        message: '',
+        data: { roomId },
+      };
+    }
+    return response.data as DeleteDailyGreetingResponse;

🏁 Script executed:

#!/bin/bash
# 삭제 API 응답 바디 유무/스키마 사용처 점검
rg -n -C2 --type ts "DeleteDailyGreetingResponse|deleteDailyGreeting|daily-greeting"

Length of output: 2800


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== src/api/rooms/deleteDailyGreeting.ts ====="
sed -n '1,200p' src/api/rooms/deleteDailyGreeting.ts || true

echo
echo "===== src/components/today-words/MessageList/MessageList.tsx (lines ~90-130) ====="
sed -n '90,130p' src/components/today-words/MessageList/MessageList.tsx || true

Length of output: 2020


DELETE 204(No Content) 대응 — deleteDailyGreeting에서 2xx 응답을 성공으로 정규화하세요.

현재 deleteDailyGreeting는 response.data를 바로 반환하므로 서버가 204를 반환하면 호출부의 result.isSuccess 검사에서 실패로 처리됩니다. API 함수에서 2xx 응답을 성공으로 정규화하도록 수정하세요.
수정 파일: src/api/rooms/deleteDailyGreeting.ts (호출부 참고: src/components/today-words/MessageList/MessageList.tsx, result.isSuccess 검사).

-    const response = await apiClient.delete<DeleteDailyGreetingResponse>(
-      `/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
-    );
-
-    return response.data;
+    const response = await apiClient.delete(
+      `/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
+    );
+    // 2xx는 성공으로 정규화
+    if (response.status >= 200 && response.status < 300) {
+      return (response.data as DeleteDailyGreetingResponse) ?? {
+        isSuccess: true,
+        code: response.status,
+        message: '',
+        data: { roomId },
+      };
+    }
+    return response.data as DeleteDailyGreetingResponse;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const response = await apiClient.delete<DeleteDailyGreetingResponse>(
`/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
);
return response.data;
} catch (error) {
try {
const response = await apiClient.delete(
`/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
);
// 2xx는 성공으로 정규화
if (response.status >= 200 && response.status < 300) {
return (response.data as DeleteDailyGreetingResponse) ?? {
isSuccess: true,
code: response.status,
message: '',
data: { roomId },
};
}
return response.data as DeleteDailyGreetingResponse;
} catch (error) {
🤖 Prompt for AI Agents
In src/api/rooms/deleteDailyGreeting.ts around lines 18-24, the function returns
response.data directly which fails when the server responds with 204 No Content;
change the function to treat any 2xx status as success and return a normalized
value. Update the try block to check response.status (200 <= status < 300) and
then return response.data if present, otherwise return a normalized empty value
matching DeleteDailyGreetingResponse (e.g., {} cast to
DeleteDailyGreetingResponse or a small success object) so callers relying on
result.isSuccess treat 204 as success.

console.error('오늘의 한마디 삭제 API 오류:', error);
throw error;
}
};
Loading