- {comments.map((c, idx) => (
-
+ {comments.map((comment, index) => (
+
- {c.authorInitial}
+ {comment.authorInitial}
- {c.authorName}
- {c.date}
+ {comment.author}
+
+ {comment.board} • {comment.date}
+
+ {/* 작성자 하트 배지는 필요시 로직 추가 */}
- {c.content}
+ {comment.content}
- {c.likes}
+ {comment.likes}
+
+
+ 답글
- {idx < comments.length - 1 &&
}
+ {index < comments.length - 1 &&
}
))}
{comments.length === 0 && (
@@ -421,4 +317,4 @@ export default function PostDetail() {
);
-}
\ No newline at end of file
+}
diff --git a/src/pages/TagSettings.tsx b/src/pages/TagSettings.tsx
index 984fbc2..b50bc0b 100644
--- a/src/pages/TagSettings.tsx
+++ b/src/pages/TagSettings.tsx
@@ -5,7 +5,7 @@ import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { X } from 'lucide-react';
-import useTagStore from '@/stores/tagStore';
+import { useTagStore }from '@/stores/tagStore';
export default function TagSettings() {
const [input, setInput] = useState('');
@@ -68,7 +68,7 @@ export default function TagSettings() {
) : (
- {subscribedTags.map((tag) => (
+ {subscribedTags.map((tag: string) => (
handleRemoveTag(tag)}
className="ml-1 text-gray-500 hover:text-red-500"
+ aria-label={`${tag} 태그 삭제`}
>
diff --git a/src/services/api.ts b/src/services/api.ts
index f42940e..f4f85f7 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -10,7 +10,7 @@ const api = axios.create({
headers: {
'Content-Type': 'application/json',
},
- withCredentials: true, // ✅ 이거 추가: refresh 쿠키 보내려면 필수
+ withCredentials: true,
});
// 🔗 백엔드 연결: 요청 인터셉터 (Bearer 토큰 자동 추가)
@@ -29,49 +29,38 @@ api.interceptors.request.use(
);
// 🔗 백엔드 연결: 응답 인터셉터 (에러 처리)
-// 🔗 백엔드 연결: 응답 인터셉터 (AccessToken 자동 재발급)
api.interceptors.response.use(
(response) => response,
async (error) => {
- // error.response, error.config 둘 다 있어야 인터셉터 실행
- if (!error.response || !error.config) {
- return Promise.reject(error);
- }
+ if (!error.response || !error.config) return Promise.reject(error);
- // 원래 요청 설정
- const original = error.config as any;
+ const original = error.config;
- // ★ Access Token 만료 → 401 처리 (재시도 플래그로 무한루프 방지)
+ // 401 처리 + 무한 루프 방지
if (error.response.status === 401 && !original._retry) {
original._retry = true;
-
try {
- // 1) Refresh API 호출 (api가 아니라 axios 기본 인스턴스 사용!)
const refreshResponse = await axios.post(
`${API_BASE_URL}/auth/refresh`,
{},
- { withCredentials: true } // 쿠키 포함
+ { withCredentials: true }
);
const { accessToken, user } = refreshResponse.data;
- // 2) Zustand에 Access Token & User 갱신
+ // store 갱신
useAuthStore.getState().setAuth(accessToken, user);
- // 3) 원래 요청에 새 토큰 붙여서 재전송
- original.headers = original.headers || {};
+ // 기존 요청 재전송
original.headers.Authorization = `Bearer ${accessToken}`;
-
return api(original);
} catch (refreshError) {
- // RefreshToken도 만료 → 강제 로그아웃
useAuthStore.getState().clearAuth();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
- // 그 외 에러는 그대로 throw
return Promise.reject(error);
}
);
@@ -152,10 +141,20 @@ export const deletePost = async (id: string) => {
export const searchPosts = async (query: string) => {
const response = await api.get(`/posts/search`, { params: { query } });
- return response.data.map((post: any) => ({
+
+ type ApiTag = string | { name: string };
+
+ type ApiPost = {
+ tags?: ApiTag[] | null;
+ [key: string]: unknown; // 나머지 필드는 뭐가 오든 허용
+ };
+
+ return response.data.map((post: ApiPost) => ({
...post,
tags: Array.isArray(post.tags)
- ? post.tags.map((t: any) => (typeof t === 'object' ? t.name : t))
+ ? post.tags.map((t) =>
+ typeof t === 'object' && t !== null ? t.name : t
+ )
: [],
}));
};
@@ -250,6 +249,36 @@ export const deleteComment = async (id: string, hard: boolean = false) => {
return response.data;
};
+// GET /comments/me - 내가 쓴 댓글 조회
+export interface CommentReactions {
+ likeCount: number;
+ dislikeCount: number;
+ myReaction: 'LIKE' | 'DISLIKE' | null;
+}
+
+export interface CommentAuthor {
+ id: string;
+ displayName: string;
+ email: string;
+ nicknameColor: string;
+}
+
+export interface MyComment {
+ id: string;
+ postId: string;
+ content: string;
+ status: 'ACTIVE' | 'DELETED';
+ createdAt: string;
+ updatedAt: string;
+ reactions: CommentReactions;
+ author: CommentAuthor;
+}
+
+export const getMyComments = async (): Promise => {
+ const response = await api.get('/comments/me');
+ return response.data;
+};
+
// ========================================
// 🔗 Reactions API
// ========================================
@@ -305,4 +334,4 @@ export const getIsScraped = async (postId: string) => {
// 🔗 Favorite Tags API (내 즐겨찾기 태그)
// ========================================
-export default api;
+export default api;
\ No newline at end of file
diff --git a/src/stores/tagStore.ts b/src/stores/tagStore.ts
index 3c5dd81..bbf8cca 100644
--- a/src/stores/tagStore.ts
+++ b/src/stores/tagStore.ts
@@ -1,16 +1,19 @@
-// src/stores/tagStore.ts
import { create } from 'zustand';
const STORAGE_KEY = 'subscribedTags';
+// localStorage 에서 태그 배열 불러오기
const loadFromStorage = (): string[] => {
if (typeof window === 'undefined') return [];
+
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
+
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
- return parsed.filter((t) => typeof t === 'string');
+ // 문자열만 필터링
+ return parsed.filter((t): t is string => typeof t === 'string');
}
return [];
} catch {
@@ -18,48 +21,57 @@ const loadFromStorage = (): string[] => {
}
};
+// localStorage 에 저장
const saveToStorage = (tags: string[]) => {
if (typeof window === 'undefined') return;
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(tags));
};
-interface TagState {
+// ✅ AppSidebar 에서 사용할 스토어 상태 타입
+export interface TagStoreState {
+ /** 구독된 태그 리스트 */
subscribedTags: string[];
- setTags: (tags: string[]) => void;
- addTag: (tag: string) => void;
- removeTag: (tag: string) => void;
- hydrate: () => void; // localStorage → store로 복원
+
+ /** 태그 하나 추가 */
+ subscribeTag: (tag: string) => void;
+
+ /** 태그 하나 제거 */
+ unsubscribeTag: (tag: string) => void;
+
+ /** 전부 비우기 */
+ clearSubscribedTags: () => void;
+
+ /** localStorage 값으로 상태 다시 동기화 */
+ hydrate: () => void;
}
-const useTagStore = create((set, get) => ({
- subscribedTags: [],
+// ✅ zustand 스토어 생성
+export const useTagStore = create((set, get) => ({
+ subscribedTags: loadFromStorage(),
hydrate: () => {
- const tags = loadFromStorage();
- set({ subscribedTags: tags });
- },
-
- setTags: (tags) => {
- set({ subscribedTags: tags });
- saveToStorage(tags);
+ const loaded = loadFromStorage();
+ set({ subscribedTags: loaded });
},
- addTag: (tag) => {
- const name = tag.trim();
- if (!name) return;
+ subscribeTag: (tag) => {
const current = get().subscribedTags;
- if (current.includes(name)) return;
- const next = [...current, name];
- set({ subscribedTags: next });
+ const next = Array.from(new Set([...current, tag])); // 중복 제거
+
saveToStorage(next);
+ set({ subscribedTags: next });
},
- removeTag: (tag) => {
+ unsubscribeTag: (tag) => {
const current = get().subscribedTags;
const next = current.filter((t) => t !== tag);
- set({ subscribedTags: next });
+
saveToStorage(next);
+ set({ subscribedTags: next });
},
-}));
-export default useTagStore;
+ clearSubscribedTags: () => {
+ saveToStorage([]);
+ set({ subscribedTags: [] });
+ },
+}));
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 2257f2d..db9f57d 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -31,5 +31,5 @@
]
}
},
- "include": ["src"]
+ "include": ["src", "tailwind.config.js"]
}
diff --git a/tsconfig.json b/tsconfig.json
index afa1ec6..743e643 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,17 +1,12 @@
{
"files": [],
"references": [
- {
- "path": "./tsconfig.app.json"
- },
- {
- "path": "./tsconfig.node.json"
- }
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
],
"compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@/*": ["./src/*"]
- }
+ "composite": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true
}
-}
\ No newline at end of file
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
index 7a77bab..92001dd 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -1,8 +1,8 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2023",
- "lib": ["ES2023"],
+ "target": "ESNext",
+ "lib": ["ESNext"],
"module": "ESNext",
"types": [],
"skipLibCheck": true,