From 39b7f5cedf8fcd17cd1220dbabbcf99f32de0988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=84=B1=EB=B9=88?= Date: Mon, 2 Feb 2026 11:56:23 +0900 Subject: [PATCH 1/2] Add user locale support & Prettier setup Persist and sync user locale: add PUT /api/users/locale and ensureUsersLocaleColumn utility; read/write locale in auth flow and store it in the session; add SessionProviderWrapper and update SessionToRedux to initialize and dispatch locale (with detectLocale fallback and sessionStorage flag); LanguageCard now saves selected locale to the API. Also add Prettier config/ignore, npm scripts (format, format:check, lint:fix) and eslint-config-prettier/prettier deps. Misc: many small formatting/cleanup changes across files (one-liners, error message formatting, refactors) to conform to the new style. --- .eslintrc.json | 2 +- .prettierignore | 24 ++++ .prettierrc | 7 ++ README.md | 3 +- auth.config.ts | 4 +- auth.ts | 11 +- package-lock.json | 29 +++++ package.json | 5 + scripts/seed.js | 8 +- src/app/api/comments/deleteComment.ts | 7 +- src/app/api/comments/postComment.ts | 7 +- src/app/api/comments/putComment.ts | 6 +- src/app/api/image/upload/route.ts | 5 +- src/app/api/posts/deletePost.ts | 6 +- src/app/api/posts/generateMarkdown.ts | 7 +- src/app/api/posts/generateThumbnail.ts | 3 +- src/app/api/posts/getPost.ts | 4 +- src/app/api/posts/list/route.ts | 6 +- src/app/api/posts/postPost.ts | 14 +-- src/app/api/posts/putPost.ts | 6 +- src/app/api/posts/recent/route.ts | 5 +- src/app/api/posts/togglePostLike.ts | 6 +- src/app/api/projects/getPreview.ts | 8 +- src/app/api/projects/preview/route.ts | 10 +- src/app/api/tags/deletetag.ts | 6 +- src/app/api/tags/postTag.ts | 14 +-- src/app/api/tags/putTag.ts | 16 +-- src/app/api/users/locale/route.ts | 53 +++++++++ src/app/global-error.tsx | 6 +- src/app/layout.tsx | 54 ++++----- src/app/login/page.tsx | 6 +- src/app/mypage/AvatarChange.tsx | 12 +- src/app/mypage/MypageContent.tsx | 9 +- src/app/page.tsx | 4 +- src/app/posts/[index]/page.tsx | 2 +- src/app/posts/layout.tsx | 6 +- .../common/SessionProviderWrapper.tsx | 16 +++ src/components/common/SessionToRedux.tsx | 17 ++- .../common/background/ColorBends.tsx | 21 +--- .../common/header/PostsHeader/index.tsx | 20 +--- .../DefaultTerminal/styles.module.scss | 3 +- .../common/theme/ThemeButton/index.tsx | 20 +--- src/components/main/card/FigmaCard/index.tsx | 7 +- .../main/card/ImageCardShadow/index.tsx | 9 +- .../card/ImageCardShadow/styles.module.scss | 3 +- .../main/card/LanguageCard/index.tsx | 23 ++-- .../main/card/LanguageCard/styles.module.scss | 4 +- src/components/main/card/MainCard/index.tsx | 23 +--- src/components/main/card/OauthCard/index.tsx | 10 +- .../card/PostCard/PopularPostCard/index.tsx | 11 +- .../card/PostCard/RecentPostCard/index.tsx | 12 +- src/components/main/card/RateCard/index.tsx | 9 +- src/components/main/card/RecentCard/index.tsx | 7 +- .../main/card/SearchCard/SearchList/index.tsx | 6 +- src/components/main/card/SearchCard/index.tsx | 6 +- .../main/card/WeatherCard/index.tsx | 19 +-- src/components/main/card/index.ts | 6 +- .../main/container/InfoContainer/index.tsx | 8 +- .../main/container/PostContainer/index.tsx | 13 +- .../ProjectsSection/CardSwap/index.tsx | 60 +++------- src/components/main/container/index.ts | 8 +- .../GenerateMarkdownFromPrompt/index.tsx | 8 +- .../styles.module.scss | 5 +- src/components/posts/TagManager/index.tsx | 39 ++---- .../posts/TagManagerModal/index.tsx | 111 ++++++------------ .../posts/TagManagerModal/styles.module.scss | 3 +- .../Comments/CommentForm/styles.module.scss | 10 +- .../posts/[id]/Comments/CommentItem/index.tsx | 11 +- src/components/posts/[id]/Comments/index.tsx | 11 +- .../posts/[id]/PostLikeButton/index.tsx | 24 ++-- .../[id]/PostLikeButton/styles.module.scss | 2 +- .../posts/[id]/PostNavigation/index.tsx | 15 +-- .../[id]/ThumbnailGenerateButton/index.tsx | 10 +- .../styles.module.scss | 5 +- src/components/posts/postListItem/index.tsx | 23 +--- .../posts/postTagSelectContainer/index.tsx | 15 +-- .../tagChip/styles.module.scss | 6 +- src/components/posts/postsList/index.tsx | 13 +- src/constants/editor/commands/image.tsx | 5 +- src/constants/editor/commands/scroll.tsx | 9 +- src/constants/editor/toolbars.ts | 5 +- src/constants/index.ts | 6 +- src/constants/metadata.ts | 6 +- src/constants/projects.ts | 4 +- src/constants/queryKey.ts | 9 +- src/hooks/useCity.ts | 10 +- src/hooks/useComments.ts | 18 +-- src/hooks/useImageUpload.ts | 4 +- src/hooks/usePostsList.ts | 5 +- src/hooks/useRecentPosts.ts | 11 +- src/hooks/useTags.ts | 4 +- src/hooks/useWeather.ts | 6 +- src/store/index.ts | 3 +- src/store/modules/post.ts | 10 +- src/store/modules/snackbar.ts | 3 +- src/store/modules/tag.ts | 3 +- src/store/modules/theme.ts | 3 +- src/styles/globals.css | 5 +- src/types/next-auth.d.ts | 2 + src/types/user.d.ts | 2 + src/utils/compareTime.ts | 4 +- src/utils/detectSystem.ts | 23 +++- src/utils/ensureUsersLocaleColumn.ts | 26 ++++ 103 files changed, 499 insertions(+), 720 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 src/app/api/users/locale/route.ts create mode 100644 src/components/common/SessionProviderWrapper.tsx create mode 100644 src/utils/ensureUsersLocaleColumn.ts diff --git a/.eslintrc.json b/.eslintrc.json index bffb357..4d765f2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "next/core-web-vitals" + "extends": ["next/core-web-vitals", "prettier"] } diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5069dc4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,24 @@ +# dependencies & build +node_modules +.next +out +build +.vercel + +# generated / lock +package-lock.json +*.tsbuildinfo +next-env.d.ts + +# assets +public/workbox-*.js +public/sw.js + +# docs / agents +.agents +*.md +!README.md + +# misc +.DS_Store +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5733039 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "tabWidth": 2, + "printWidth": 100 +} diff --git a/README.md b/README.md index c6861ae..1a9908b 100644 --- a/README.md +++ b/README.md @@ -25,4 +25,5 @@ Canary Lab is a technology blog where we explore the latest trends, share insigh - OAuth login: GitHub, Google (optional). ## Environment Variables (OAuth)- **GitHub**: `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET` -- **Google**: `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET` (Google Cloud Console에서 OAuth 2.0 클라이언트 ID 생성 후 설정) \ No newline at end of file + +- **Google**: `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET` (Google Cloud Console에서 OAuth 2.0 클라이언트 ID 생성 후 설정) diff --git a/auth.config.ts b/auth.config.ts index 8193220..627256b 100644 --- a/auth.config.ts +++ b/auth.config.ts @@ -8,13 +8,13 @@ export const authConfig = { callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; - const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); + const isOnDashboard = nextUrl.pathname.startsWith("/dashboard"); if (isOnDashboard) { if (isLoggedIn) return true; return false; } else if (isLoggedIn) { - return Response.redirect(new URL('/dashboard', nextUrl)); + return Response.redirect(new URL("/dashboard", nextUrl)); } return true; }, diff --git a/auth.ts b/auth.ts index e258515..e2cfab8 100644 --- a/auth.ts +++ b/auth.ts @@ -9,6 +9,7 @@ import { sql } from "@vercel/postgres"; import { ensureAccountsTable } from "./src/utils/ensureAccountsTable"; import { mergeUserIntoUser } from "./src/utils/mergeUserIntoUser"; import { ensureUsersUserTypeColumn } from "./src/utils/ensureUsersUserTypeColumn"; +import { ensureUsersLocaleColumn } from "./src/utils/ensureUsersLocaleColumn"; import { cookies } from "next/headers"; /** 세션 만료: 30일 (Auth.js 기본값, 사용성·보안 균형) */ @@ -181,6 +182,8 @@ export const { const userName = session.user.name || "Unknown"; const userImage = session.user.image || null; + await ensureUsersUserTypeColumn(); + await ensureUsersLocaleColumn(); await sql` INSERT INTO users (email, name, image, last_login, login_count) VALUES (${userEmail}, ${userName}, ${userImage}, CURRENT_TIMESTAMP, 1) @@ -189,10 +192,8 @@ export const { last_login = CURRENT_TIMESTAMP, login_count = users.login_count + 1 `; - - await ensureUsersUserTypeColumn(); const { rows: userRows } = await sql` - SELECT id, image, user_type FROM users WHERE email = ${userEmail} + SELECT id, image, user_type, locale FROM users WHERE email = ${userEmail} `; const userId = userRows?.[0]?.id; if (userRows?.[0]?.image != null) { @@ -202,6 +203,10 @@ export const { if (userType === "admin" || userType === "normal") { session.user.userType = userType; } + const userLocale = userRows?.[0]?.locale; + if (userLocale === "ko" || userLocale === "en") { + session.user.locale = userLocale; + } if (userId) { session.user.id = userId; const { rows: accountRows } = await sql` diff --git a/package-lock.json b/package-lock.json index 2ecfa55..a333440 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,9 @@ "dotenv": "^16.3.1", "eslint": "^8", "eslint-config-next": "14.0.4", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-react": "^7.33.2", + "prettier": "^3.4.2", "typescript": "^5" }, "engines": { @@ -7161,6 +7163,18 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -12158,6 +12172,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", diff --git a/package.json b/package.json index 565f5df..b9d5ce1 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "build": "next build", "start": "next start", "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,scss,md}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,css,scss,md}\"", "seed": "node -r dotenv/config ./scripts/seed.js" }, "dependencies": { @@ -84,6 +87,8 @@ "eslint": "^8", "eslint-config-next": "14.0.4", "eslint-plugin-react": "^7.33.2", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.4.2", "typescript": "^5" } } diff --git a/scripts/seed.js b/scripts/seed.js index 764ea44..eef494f 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -71,8 +71,7 @@ async function seedPosts(client) { // Insert data into the "posts" table const insertedPosts = await Promise.all( posts.map(async (post) => { - const { title, content, tags, createdAt, updatedAt, likes, views } = - post; + const { title, content, tags, createdAt, updatedAt, likes, views } = post; return client.query( ` @@ -142,8 +141,5 @@ main() console.log("Seed completed successfully"); }) .catch((err) => { - console.error( - "An error occurred while attempting to seed the database:", - err - ); + console.error("An error occurred while attempting to seed the database:", err); }); diff --git a/src/app/api/comments/deleteComment.ts b/src/app/api/comments/deleteComment.ts index ec0b46d..2f73ab3 100644 --- a/src/app/api/comments/deleteComment.ts +++ b/src/app/api/comments/deleteComment.ts @@ -14,7 +14,6 @@ export async function deleteComment(commentId: string): Promise { // 인증 체크 const session = await auth(); - console.log("session", session); if (!session?.user) { throw new Error("Unauthorized: 로그인이 필요합니다."); } @@ -63,10 +62,6 @@ export async function deleteComment(commentId: string): Promise { } } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "댓글 삭제 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "댓글 삭제 중 오류가 발생했습니다."); } } diff --git a/src/app/api/comments/postComment.ts b/src/app/api/comments/postComment.ts index 306a3a3..ea94881 100644 --- a/src/app/api/comments/postComment.ts +++ b/src/app/api/comments/postComment.ts @@ -22,7 +22,6 @@ export async function postComment({ // 인증 체크 const session = await auth(); - console.log("session", session); if (!session?.user) { throw new Error("Unauthorized: 로그인이 필요합니다."); } @@ -103,10 +102,6 @@ export async function postComment({ return camelcaseKeys(commentWithUser[0], { deep: true }) as IComment; } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "댓글 작성 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "댓글 작성 중 오류가 발생했습니다."); } } diff --git a/src/app/api/comments/putComment.ts b/src/app/api/comments/putComment.ts index 8f40522..282ca0d 100644 --- a/src/app/api/comments/putComment.ts +++ b/src/app/api/comments/putComment.ts @@ -74,10 +74,6 @@ export async function putComment({ } } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "댓글 수정 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "댓글 수정 중 오류가 발생했습니다."); } } diff --git a/src/app/api/image/upload/route.ts b/src/app/api/image/upload/route.ts index 36a00ef..4f4d8bf 100644 --- a/src/app/api/image/upload/route.ts +++ b/src/app/api/image/upload/route.ts @@ -35,9 +35,6 @@ export async function POST(request: Request): Promise { return NextResponse.json(jsonResponse); } catch (error) { - return NextResponse.json( - { error: (error as Error).message }, - { status: 400 } - ); + return NextResponse.json({ error: (error as Error).message }, { status: 400 }); } } diff --git a/src/app/api/posts/deletePost.ts b/src/app/api/posts/deletePost.ts index 160947e..f49ed38 100644 --- a/src/app/api/posts/deletePost.ts +++ b/src/app/api/posts/deletePost.ts @@ -40,10 +40,6 @@ export async function deletePost(index: number) { redirect("/posts"); } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "게시물 삭제 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "게시물 삭제 중 오류가 발생했습니다."); } } diff --git a/src/app/api/posts/generateMarkdown.ts b/src/app/api/posts/generateMarkdown.ts index 2ea8571..7ce13dd 100644 --- a/src/app/api/posts/generateMarkdown.ts +++ b/src/app/api/posts/generateMarkdown.ts @@ -9,9 +9,7 @@ const PROMPT_MAX_LENGTH = 2000; * 사용자가 입력한 프롬프트를 바탕으로 AI가 블로그 게시글용 마크다운 본문을 생성합니다. * OPENAI_API_KEY 환경 변수 필요. (채팅 API는 무료 크레딧으로 사용 가능한 경우 있음) */ -export async function generateMarkdownFromPrompt( - userPrompt: string -): Promise { +export async function generateMarkdownFromPrompt(userPrompt: string): Promise { const session = await auth(); if (!session?.user) { throw new Error("Unauthorized: 로그인이 필요합니다."); @@ -54,7 +52,8 @@ export async function generateMarkdownFromPrompt( return content; } catch (err: unknown) { - const status = err && typeof err === "object" && "status" in err ? (err as { status: number }).status : null; + const status = + err && typeof err === "object" && "status" in err ? (err as { status: number }).status : null; const msg = err instanceof Error ? err.message : String(err ?? ""); const isQuotaOr429 = status === 429 || /quota|billing|exceeded/i.test(msg); diff --git a/src/app/api/posts/generateThumbnail.ts b/src/app/api/posts/generateThumbnail.ts index 196c275..4d64d67 100644 --- a/src/app/api/posts/generateThumbnail.ts +++ b/src/app/api/posts/generateThumbnail.ts @@ -64,7 +64,8 @@ export async function generatePostThumbnail( }); imageUrl = response.data?.[0]?.url ?? ""; } catch (err: unknown) { - const status = err && typeof err === "object" && "status" in err ? (err as { status: number }).status : null; + const status = + err && typeof err === "object" && "status" in err ? (err as { status: number }).status : null; const msg = err instanceof Error ? err.message : String(err ?? ""); const isQuotaOr429 = status === 429 || /quota|billing|exceeded/i.test(msg); throw new Error( diff --git a/src/app/api/posts/getPost.ts b/src/app/api/posts/getPost.ts index b0732d5..ad6b58c 100644 --- a/src/app/api/posts/getPost.ts +++ b/src/app/api/posts/getPost.ts @@ -28,9 +28,7 @@ export async function getPost(index: number): Promise { } catch (error) { console.error("Database Error:", error); throw new Error( - error instanceof Error - ? error.message - : "게시물을 불러오는 중 오류가 발생했습니다." + error instanceof Error ? error.message : "게시물을 불러오는 중 오류가 발생했습니다." ); } } diff --git a/src/app/api/posts/list/route.ts b/src/app/api/posts/list/route.ts index b698418..7f0263f 100644 --- a/src/app/api/posts/list/route.ts +++ b/src/app/api/posts/list/route.ts @@ -31,10 +31,6 @@ export async function GET(request: Request) { return NextResponse.json(camelcaseKeys(rows, { deep: true })); } catch (error) { console.error("Database Error:", error); - return NextResponse.json( - { error: "Failed to fetch posts data." }, - { status: 500 } - ); + return NextResponse.json({ error: "Failed to fetch posts data." }, { status: 500 }); } } - diff --git a/src/app/api/posts/postPost.ts b/src/app/api/posts/postPost.ts index f73e0f6..6d4bb4e 100644 --- a/src/app/api/posts/postPost.ts +++ b/src/app/api/posts/postPost.ts @@ -11,13 +11,7 @@ import { auth } from "@/auth"; * @param title 게시물 제목 * @param markdownValue 게시물 내용 */ -export async function postPost({ - title, - markdownValue, -}: { - title: string; - markdownValue: string; -}) { +export async function postPost({ title, markdownValue }: { title: string; markdownValue: string }) { noStore(); // 인증 체크 @@ -52,10 +46,6 @@ export async function postPost({ redirect(`/posts/${postIndex}`); } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "게시물 생성 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "게시물 생성 중 오류가 발생했습니다."); } } diff --git a/src/app/api/posts/putPost.ts b/src/app/api/posts/putPost.ts index 00a6934..c4bb507 100644 --- a/src/app/api/posts/putPost.ts +++ b/src/app/api/posts/putPost.ts @@ -58,10 +58,6 @@ export async function putPost({ redirect(`/posts/${index}`); } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "게시물 수정 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "게시물 수정 중 오류가 발생했습니다."); } } diff --git a/src/app/api/posts/recent/route.ts b/src/app/api/posts/recent/route.ts index e23e773..9432a78 100644 --- a/src/app/api/posts/recent/route.ts +++ b/src/app/api/posts/recent/route.ts @@ -38,9 +38,6 @@ export async function GET(request: Request) { return NextResponse.json(camelcaseKeys(combinedPosts, { deep: true })); } catch (error) { console.error("Database Error:", error); - return NextResponse.json( - { error: "Failed to fetch recent posts data." }, - { status: 500 } - ); + return NextResponse.json({ error: "Failed to fetch recent posts data." }, { status: 500 }); } } diff --git a/src/app/api/posts/togglePostLike.ts b/src/app/api/posts/togglePostLike.ts index b001bfb..8af387b 100644 --- a/src/app/api/posts/togglePostLike.ts +++ b/src/app/api/posts/togglePostLike.ts @@ -97,10 +97,6 @@ export async function togglePostLike(postIndex: number): Promise { return isNowLiked; } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "좋아요 처리 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "좋아요 처리 중 오류가 발생했습니다."); } } diff --git a/src/app/api/projects/getPreview.ts b/src/app/api/projects/getPreview.ts index a0618d3..2c74b95 100644 --- a/src/app/api/projects/getPreview.ts +++ b/src/app/api/projects/getPreview.ts @@ -54,11 +54,7 @@ export async function getProjectPreview(url: string): Promise { +export async function getProjectPreviews(urls: string[]): Promise { const results = await Promise.all(urls.map((url) => getProjectPreview(url))); - return results.filter( - (p): p is ProjectPreview => p !== null - ); + return results.filter((p): p is ProjectPreview => p !== null); } diff --git a/src/app/api/projects/preview/route.ts b/src/app/api/projects/preview/route.ts index 10407c1..4ed4f42 100644 --- a/src/app/api/projects/preview/route.ts +++ b/src/app/api/projects/preview/route.ts @@ -6,18 +6,12 @@ export type { ProjectPreview } from "../getPreview"; export async function GET(request: NextRequest) { const url = request.nextUrl.searchParams.get("url"); if (!url) { - return NextResponse.json( - { error: "유효한 url 쿼리가 필요합니다." }, - { status: 400 } - ); + return NextResponse.json({ error: "유효한 url 쿼리가 필요합니다." }, { status: 400 }); } const payload = await getProjectPreview(url); if (!payload) { - return NextResponse.json( - { error: "미리보기를 가져올 수 없습니다." }, - { status: 502 } - ); + return NextResponse.json({ error: "미리보기를 가져올 수 없습니다." }, { status: 502 }); } return NextResponse.json(payload); diff --git a/src/app/api/tags/deletetag.ts b/src/app/api/tags/deletetag.ts index 9945c54..216db03 100644 --- a/src/app/api/tags/deletetag.ts +++ b/src/app/api/tags/deletetag.ts @@ -40,10 +40,6 @@ export async function deleteTag(id: string) { revalidatePath("/posts"); } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "태그 삭제 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "태그 삭제 중 오류가 발생했습니다."); } } diff --git a/src/app/api/tags/postTag.ts b/src/app/api/tags/postTag.ts index 364fd65..64d7932 100644 --- a/src/app/api/tags/postTag.ts +++ b/src/app/api/tags/postTag.ts @@ -9,13 +9,7 @@ import { auth } from "@/auth"; * @param name 태그 이름 * @param color 태그 색상 */ -export async function postTag({ - name, - color, -}: { - name: string; - color: string; -}) { +export async function postTag({ name, color }: { name: string; color: string }) { noStore(); // 인증 체크 @@ -48,10 +42,6 @@ export async function postTag({ return rows[0]; } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "태그 생성 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "태그 생성 중 오류가 발생했습니다."); } } diff --git a/src/app/api/tags/putTag.ts b/src/app/api/tags/putTag.ts index 4bf3e1b..e73f691 100644 --- a/src/app/api/tags/putTag.ts +++ b/src/app/api/tags/putTag.ts @@ -10,15 +10,7 @@ import { auth } from "@/auth"; * @param name 태그 이름 * @param color 태그 색상 */ -export async function putTag({ - id, - name, - color, -}: { - id: string; - name: string; - color: string; -}) { +export async function putTag({ id, name, color }: { id: string; name: string; color: string }) { noStore(); // 인증 체크 @@ -57,10 +49,6 @@ export async function putTag({ revalidatePath("/posts"); } catch (error) { console.error("Database Error:", error); - throw new Error( - error instanceof Error - ? error.message - : "태그 수정 중 오류가 발생했습니다." - ); + throw new Error(error instanceof Error ? error.message : "태그 수정 중 오류가 발생했습니다."); } } diff --git a/src/app/api/users/locale/route.ts b/src/app/api/users/locale/route.ts new file mode 100644 index 0000000..93711f9 --- /dev/null +++ b/src/app/api/users/locale/route.ts @@ -0,0 +1,53 @@ +import { auth } from "@/auth"; +import { sql } from "@vercel/postgres"; + +const ALLOWED_LOCALES = ["ko", "en"] as const; + +/** + * 로그인한 사용자의 locale을 DB에 저장합니다. + * PUT body: { locale: "ko" | "en" } + */ +export async function PUT(request: Request) { + const session = await auth(); + if (!session?.user?.email) { + return new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + + let body: { locale?: string }; + try { + body = await request.json(); + } catch { + return new Response(JSON.stringify({ error: "Invalid JSON" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + const locale = body.locale; + if (!locale || !ALLOWED_LOCALES.includes(locale as (typeof ALLOWED_LOCALES)[number])) { + return new Response(JSON.stringify({ error: "locale must be 'ko' or 'en'" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + try { + await sql` + UPDATE users SET locale = ${locale} + WHERE email = ${session.user.email.trim()} + `; + return new Response(JSON.stringify({ ok: true, locale }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("updateUserLocale:", error); + return new Response(JSON.stringify({ error: "Failed to update locale" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +} diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index f756105..e8d7e9a 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -4,11 +4,7 @@ import { DefaultTerminal } from "@/components/common/terminal"; import styles from "./not-found.module.scss"; import { ERROR_SEQUENCE } from "@/constants/sequence/sequence"; -export default function Error({ - error, -}: { - error: Error & { digest?: string }; -}) { +export default function Error({ error }: { error: Error & { digest?: string } }) { return (
@@ -57,26 +51,28 @@ export default async function RootLayout({ - - {/* Color Bends: 라이트/다크 테마별 색상 - https://reactbits.dev/backgrounds/color-bends */} -
- -
-
- - {children} - - -
+ + + {/* Color Bends: 라이트/다크 테마별 색상 - https://reactbits.dev/backgrounds/color-bends */} +
+ +
+
+ + {children} + + +
+
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 5cb5f51..7e110d0 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -7,9 +7,5 @@ export const metadata: Metadata = { }; export default async function LoginPage() { - return ( -
- {await LoginPageForm()} -
- ); + return
{await LoginPageForm()}
; } diff --git a/src/app/mypage/AvatarChange.tsx b/src/app/mypage/AvatarChange.tsx index e3e24ff..779992d 100644 --- a/src/app/mypage/AvatarChange.tsx +++ b/src/app/mypage/AvatarChange.tsx @@ -12,11 +12,7 @@ interface AvatarChangeProps { styles: Record; } -export function AvatarChange({ - currentUserEmail, - currentImage, - styles: s, -}: AvatarChangeProps) { +export function AvatarChange({ currentUserEmail, currentImage, styles: s }: AvatarChangeProps) { const { t } = useTranslation(); const router = useRouter(); const inputRef = useRef(null); @@ -76,7 +72,11 @@ export function AvatarChange({ {uploading ? t("common.loading") : t("mypage.avatar.change")} - {error &&

{error}

} + {error && ( +

+ {error} +

+ )} ); } diff --git a/src/app/mypage/MypageContent.tsx b/src/app/mypage/MypageContent.tsx index 2210626..a645a39 100644 --- a/src/app/mypage/MypageContent.tsx +++ b/src/app/mypage/MypageContent.tsx @@ -15,13 +15,10 @@ export function MypageContent({ session }: MypageContentProps) { const { t } = useTranslation(); const { name, email, image, providers, currentProvider } = session.user; - const currentLabel = currentProvider - ? t(`main.providers.${currentProvider}`) + const currentLabel = currentProvider ? t(`main.providers.${currentProvider}`) : null; + const linkedList = providers?.length + ? providers.map((p) => t(`main.providers.${p}`)).join(", ") : null; - const linkedList = - providers?.length - ? providers.map((p) => t(`main.providers.${p}`)).join(", ") - : null; return (
diff --git a/src/app/page.tsx b/src/app/page.tsx index 7bc3225..e9a8fb3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -32,9 +32,7 @@ export default async function Home() {
-
+
diff --git a/src/app/posts/[index]/page.tsx b/src/app/posts/[index]/page.tsx index c73caf6..7cdb207 100644 --- a/src/app/posts/[index]/page.tsx +++ b/src/app/posts/[index]/page.tsx @@ -16,7 +16,7 @@ type Props = { export async function generateMetadata( { params }: Props, - parent: ResolvingMetadata, + parent: ResolvingMetadata ): Promise { const { index } = await params; const postIndex = Number(index); diff --git a/src/app/posts/layout.tsx b/src/app/posts/layout.tsx index 667088e..1730c88 100644 --- a/src/app/posts/layout.tsx +++ b/src/app/posts/layout.tsx @@ -1,10 +1,6 @@ import PostsHeader from "@/components/common/header/PostsHeader"; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <> diff --git a/src/components/common/SessionProviderWrapper.tsx b/src/components/common/SessionProviderWrapper.tsx new file mode 100644 index 0000000..dd1f151 --- /dev/null +++ b/src/components/common/SessionProviderWrapper.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; +import type { Session } from "next-auth"; + +interface SessionProviderWrapperProps { + session: Session | null; + children: React.ReactNode; +} + +/** + * useSession()을 쓰는 클라이언트 컴포넌트를 위해 서버에서 받은 session을 SessionProvider로 제공합니다. + */ +export function SessionProviderWrapper({ session, children }: SessionProviderWrapperProps) { + return {children}; +} diff --git a/src/components/common/SessionToRedux.tsx b/src/components/common/SessionToRedux.tsx index ffee399..fc6f514 100644 --- a/src/components/common/SessionToRedux.tsx +++ b/src/components/common/SessionToRedux.tsx @@ -3,14 +3,20 @@ import { useEffect } from "react"; import { useAppDispatch } from "@/hooks/reduxHook"; import { signIn, signOut } from "@/store/modules/user"; +import { setLocale } from "@/store/modules/language"; +import { detectLocale } from "@/utils/detectSystem"; import type { Session } from "next-auth"; +const LOCALE_INITED_KEY = "canary_locale_inited"; + interface SessionToReduxProps { session: Session | null; } /** - * 로그인된 세션을 Redux에 반영해 OauthCard·CommentForm 등에서 최신 이미지/정보가 보이도록 합니다. + * 로그인된 세션을 Redux에 반영하고, locale을 동기화합니다. + * - 로그인: DB에 저장된 locale을 세션에서 읽어 Redux에 반영 + * - 비로그인 첫 방문: 브라우저 언어로 자동 매칭 후 세션스토리지에 초기화 플래그 저장 */ export function SessionToRedux({ session }: SessionToReduxProps) { const dispatch = useAppDispatch(); @@ -18,6 +24,12 @@ export function SessionToRedux({ session }: SessionToReduxProps) { useEffect(() => { if (!session?.user?.email) { dispatch(signOut()); + // 비로그인: 첫 방문 시에만 브라우저 언어로 locale 설정 + if (typeof window !== "undefined" && !sessionStorage.getItem(LOCALE_INITED_KEY)) { + const locale = detectLocale(); + dispatch(setLocale(locale)); + sessionStorage.setItem(LOCALE_INITED_KEY, "1"); + } return; } const u = session.user; @@ -32,6 +44,9 @@ export function SessionToRedux({ session }: SessionToReduxProps) { currentProvider: u.currentProvider ?? null, }) ); + if (u.locale === "ko" || u.locale === "en") { + dispatch(setLocale(u.locale)); + } }, [session, dispatch]); return null; diff --git a/src/components/common/background/ColorBends.tsx b/src/components/common/background/ColorBends.tsx index 193b83a..462002b 100644 --- a/src/components/common/background/ColorBends.tsx +++ b/src/components/common/background/ColorBends.tsx @@ -148,10 +148,7 @@ export default function ColorBends({ const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const geometry = new THREE.PlaneGeometry(2, 2); - const uColorsArray = Array.from( - { length: MAX_COLORS }, - () => new THREE.Vector3(0, 0, 0), - ); + const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0)); const material = new THREE.ShaderMaterial({ vertexShader: vert, fragmentShader: frag, @@ -186,9 +183,7 @@ export default function ColorBends({ }); rendererRef.current = renderer; if ("outputColorSpace" in renderer) { - ( - renderer as THREE.WebGLRenderer & { outputColorSpace: string } - ).outputColorSpace = "srgb"; + (renderer as THREE.WebGLRenderer & { outputColorSpace: string }).outputColorSpace = "srgb"; } renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setClearColor(0x000000, transparent ? 0 : 1); @@ -269,16 +264,8 @@ export default function ColorBends({ const h = hex.replace("#", "").trim(); const v = h.length === 3 - ? [ - parseInt(h[0] + h[0], 16), - parseInt(h[1] + h[1], 16), - parseInt(h[2] + h[2], 16), - ] - : [ - parseInt(h.slice(0, 2), 16), - parseInt(h.slice(2, 4), 16), - parseInt(h.slice(4, 6), 16), - ]; + ? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)] + : [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255); }; diff --git a/src/components/common/header/PostsHeader/index.tsx b/src/components/common/header/PostsHeader/index.tsx index c87849b..ad384a2 100644 --- a/src/components/common/header/PostsHeader/index.tsx +++ b/src/components/common/header/PostsHeader/index.tsx @@ -26,9 +26,7 @@ const PostsHeader = () => { const isEdit = pathname.endsWith("/edit"); const index = Number(pathname.split("/")[2]); - const handleButton = async ( - type: "create" | "edit" | "delete" | "cancel" - ) => { + const handleButton = async (type: "create" | "edit" | "delete" | "cancel") => { try { setLoading(true); switch (type) { @@ -65,21 +63,15 @@ const PostsHeader = () => { <> handleButton("cancel")} > {t("common.cancel")} @@ -89,9 +81,7 @@ const PostsHeader = () => { <> {t("common.edit")} diff --git a/src/components/common/terminal/DefaultTerminal/styles.module.scss b/src/components/common/terminal/DefaultTerminal/styles.module.scss index e101057..53146f5 100644 --- a/src/components/common/terminal/DefaultTerminal/styles.module.scss +++ b/src/components/common/terminal/DefaultTerminal/styles.module.scss @@ -95,7 +95,8 @@ white-space: nowrap; overflow: hidden; border-right: 0.2em solid green; /* Cursor */ - animation: typeAndDelete 4s steps(11) infinite, + animation: + typeAndDelete 4s steps(11) infinite, blinkCursor 0.5s step-end infinite alternate; margin-top: 1.5em; } diff --git a/src/components/common/theme/ThemeButton/index.tsx b/src/components/common/theme/ThemeButton/index.tsx index 4a8be08..ccd9b46 100644 --- a/src/components/common/theme/ThemeButton/index.tsx +++ b/src/components/common/theme/ThemeButton/index.tsx @@ -3,18 +3,10 @@ import { useEffect, useMemo } from "react"; import styles from "./styles.module.scss"; import { useAppDispatch, useAppSelector } from "@/hooks/reduxHook"; -import { - setThemeWhenSystemChange, - setThemeWhenToggleClick, -} from "@/store/modules/theme"; +import { setThemeWhenSystemChange, setThemeWhenToggleClick } from "@/store/modules/theme"; import { detectTheme } from "@/utils/detectSystem"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - IconDefinition, - faDesktop, - faMoon, - faSun, -} from "@fortawesome/free-solid-svg-icons"; +import { IconDefinition, faDesktop, faMoon, faSun } from "@fortawesome/free-solid-svg-icons"; interface IProps { buttonType: Theme; @@ -45,11 +37,9 @@ const ThemeButton = ({ buttonType }: IProps) => { dispatch(setThemeWhenToggleClick(buttonType)); }; - window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", () => { - dispatch(setThemeWhenSystemChange(detectTheme())); - }); + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { + dispatch(setThemeWhenSystemChange(detectTheme())); + }); useEffect(() => { const nextTheme = theme === "system" ? detectTheme() : theme; diff --git a/src/components/main/card/FigmaCard/index.tsx b/src/components/main/card/FigmaCard/index.tsx index 74543c5..441b216 100644 --- a/src/components/main/card/FigmaCard/index.tsx +++ b/src/components/main/card/FigmaCard/index.tsx @@ -21,12 +21,7 @@ export const FigmaCard = () => {
Figma
- Figma + Figma

{t("main.figmaCard.design")}

diff --git a/src/components/main/card/ImageCardShadow/index.tsx b/src/components/main/card/ImageCardShadow/index.tsx index 44c65a1..557b129 100644 --- a/src/components/main/card/ImageCardShadow/index.tsx +++ b/src/components/main/card/ImageCardShadow/index.tsx @@ -5,15 +5,10 @@ interface IProps { canClick?: boolean; } -export const ImageCardShadow = ({ - figure = "square", - canClick = true, -}: IProps) => { +export const ImageCardShadow = ({ figure = "square", canClick = true }: IProps) => { return (
{ const dispatch = useAppDispatch(); const currentLocale = useAppSelector(selectLocale); const supportedLocales = getSupportedLocales(); + const { data: session, status } = useSession(); useEffect(() => { if (typeof document !== "undefined") { @@ -22,8 +24,19 @@ export const LanguageCard = () => { } }, [currentLocale]); - const handleSelectLocale = (locale: Locale) => { + const handleSelectLocale = async (locale: Locale) => { dispatch(setLocale(locale)); + if (status === "authenticated" && session?.user?.email) { + try { + await fetch("/api/users/locale", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ locale }), + }); + } catch { + // 저장 실패 시 Redux는 이미 반영됨 + } + } }; return ( @@ -38,9 +51,7 @@ export const LanguageCard = () => { className={styles.image} />
-

- {t("main.languageCard.selectLanguage")} -

+

{t("main.languageCard.selectLanguage")}

    {
diff --git a/src/components/main/card/PostCard/PopularPostCard/index.tsx b/src/components/main/card/PostCard/PopularPostCard/index.tsx index 7eef8f2..df5ca30 100644 --- a/src/components/main/card/PostCard/PopularPostCard/index.tsx +++ b/src/components/main/card/PostCard/PopularPostCard/index.tsx @@ -3,10 +3,7 @@ import Link from "next/link"; import styles from "./styles.module.scss"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faEye, - faHeart as regularHeart, -} from "@fortawesome/free-regular-svg-icons"; +import { faEye, faHeart as regularHeart } from "@fortawesome/free-regular-svg-icons"; import { faHeart as solidHeart } from "@fortawesome/free-solid-svg-icons"; import { useAppSelector } from "@/hooks/reduxHook"; import { isEmpty } from "lodash"; @@ -28,11 +25,7 @@ const PopularPostCard = ({ popularPosts }: IProps) => { if (isEmpty(popularPosts)) { return ( - +
게시물이 없습니다.
); diff --git a/src/components/main/card/PostCard/RecentPostCard/index.tsx b/src/components/main/card/PostCard/RecentPostCard/index.tsx index ddecc57..512c56f 100644 --- a/src/components/main/card/PostCard/RecentPostCard/index.tsx +++ b/src/components/main/card/PostCard/RecentPostCard/index.tsx @@ -4,10 +4,7 @@ import Link from "next/link"; import Image from "next/image"; import styles from "./styles.module.scss"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faEye, - faHeart as regularHeart, -} from "@fortawesome/free-regular-svg-icons"; +import { faEye, faHeart as regularHeart } from "@fortawesome/free-regular-svg-icons"; import { faHeart as solidHeart } from "@fortawesome/free-solid-svg-icons"; import { useAppSelector } from "@/hooks/reduxHook"; interface IProps { @@ -29,8 +26,7 @@ const RecentPostCard = ({ post }: IProps) => { const url = new URL(post.thumbnailUrl); const hostname = url.hostname; isVercelBlobHost = - hostname === "blob.vercel-storage.com" || - hostname.endsWith(".blob.vercel-storage.com"); + hostname === "blob.vercel-storage.com" || hostname.endsWith(".blob.vercel-storage.com"); } catch { isVercelBlobHost = false; } @@ -60,9 +56,7 @@ const RecentPostCard = ({ post }: IProps) => { {formattedDate}
- + {post.likes.length}
diff --git a/src/components/main/card/RateCard/index.tsx b/src/components/main/card/RateCard/index.tsx index ccf6b57..a33c00f 100644 --- a/src/components/main/card/RateCard/index.tsx +++ b/src/components/main/card/RateCard/index.tsx @@ -9,8 +9,7 @@ import { useTranslation } from "@/hooks/useTranslation"; export const RateCard = () => { const { t } = useTranslation(); - const svgFile = - "https://mazassumnida.wtf/api/v2/generate_badge?boj=clim03087"; + const svgFile = "https://mazassumnida.wtf/api/v2/generate_badge?boj=clim03087"; return ( { passHref > - {t("main.rateCard.solvedAcAlt")} + {t("main.rateCard.solvedAcAlt")} ); }; diff --git a/src/components/main/card/RecentCard/index.tsx b/src/components/main/card/RecentCard/index.tsx index 533083e..44f1587 100644 --- a/src/components/main/card/RecentCard/index.tsx +++ b/src/components/main/card/RecentCard/index.tsx @@ -9,12 +9,7 @@ import { useTranslation } from "@/hooks/useTranslation"; export const RecentCard = () => { const { t } = useTranslation(); return ( - +
{t("main.recentCard.recent")}
diff --git a/src/components/main/card/SearchCard/SearchList/index.tsx b/src/components/main/card/SearchCard/SearchList/index.tsx index 0ce9fc2..e19a83a 100644 --- a/src/components/main/card/SearchCard/SearchList/index.tsx +++ b/src/components/main/card/SearchCard/SearchList/index.tsx @@ -22,11 +22,7 @@ export const SearchList = ({ ) : ( searchResponse.map((post) => (
  • - + {post.title}
  • diff --git a/src/components/main/card/SearchCard/index.tsx b/src/components/main/card/SearchCard/index.tsx index 13c53a5..a2a0d15 100644 --- a/src/components/main/card/SearchCard/index.tsx +++ b/src/components/main/card/SearchCard/index.tsx @@ -54,11 +54,7 @@ export const SearchCard = () => { autoComplete="off" /> {search.length > 0 && ( - setSearch("")} - /> + setSearch("")} /> )}
    diff --git a/src/components/main/card/WeatherCard/index.tsx b/src/components/main/card/WeatherCard/index.tsx index fd17395..e20a033 100644 --- a/src/components/main/card/WeatherCard/index.tsx +++ b/src/components/main/card/WeatherCard/index.tsx @@ -7,11 +7,7 @@ import Image from "next/image"; import { useAppDispatch, useAppSelector } from "@/hooks/reduxHook"; import convertUnixTime from "@/utils/convertUnixTime"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faLocationArrow, - faSpinner, - faThermometer, -} from "@fortawesome/free-solid-svg-icons"; +import { faLocationArrow, faSpinner, faThermometer } from "@fortawesome/free-solid-svg-icons"; import { setDataReceivingTime } from "@/store/modules/weather"; import { useTranslation } from "@/hooks/useTranslation"; @@ -25,12 +21,8 @@ export const WeatherCard = () => { const city = useAppSelector((state) => state.location.city); const weatherData = useAppSelector((state) => state.weather.weatherData); - const sunrise = convertUnixTime(weatherData?.sys.sunrise) - .split(":") - .map(Number); - const sunset = convertUnixTime(weatherData?.sys.sunset) - .split(":") - .map(Number); + const sunrise = convertUnixTime(weatherData?.sys.sunrise).split(":").map(Number); + const sunset = convertUnixTime(weatherData?.sys.sunset).split(":").map(Number); const date = new Date(); const hour = date.getHours(); @@ -78,10 +70,7 @@ export const WeatherCard = () => { ) : (

    {city}

    - +
    )} {weatherLoading || ( diff --git a/src/components/main/card/index.ts b/src/components/main/card/index.ts index 44721d2..502c1d4 100644 --- a/src/components/main/card/index.ts +++ b/src/components/main/card/index.ts @@ -7,11 +7,7 @@ import { MainCard } from "@/components/main/card/MainCard"; import { MusicCard } from "@/components/main/card/MusicCard"; import { NameCard } from "@/components/main/card/NameCard"; import { OauthCard } from "@/components/main/card/OauthCard"; -import { - PopularPostCard, - RecentPostCard, - SkeletonPostCard, -} from "@/components/main/card/PostCard"; +import { PopularPostCard, RecentPostCard, SkeletonPostCard } from "@/components/main/card/PostCard"; import { ProfileCard } from "@/components/main/card/ProfileCard"; import { RateCard } from "@/components/main/card/RateCard"; import { RecentCard } from "@/components/main/card/RecentCard"; diff --git a/src/components/main/container/InfoContainer/index.tsx b/src/components/main/container/InfoContainer/index.tsx index df511cf..6517dc8 100644 --- a/src/components/main/container/InfoContainer/index.tsx +++ b/src/components/main/container/InfoContainer/index.tsx @@ -1,11 +1,5 @@ import styles from "./styles.module.scss"; -import { - GithubCard, - NameCard, - ProfileCard, - RateCard, - RoleCard, -} from "@/components/main/card"; +import { GithubCard, NameCard, ProfileCard, RateCard, RoleCard } from "@/components/main/card"; export const InfoContainer = () => { return ( diff --git a/src/components/main/container/PostContainer/index.tsx b/src/components/main/container/PostContainer/index.tsx index df650bd..6787b34 100644 --- a/src/components/main/container/PostContainer/index.tsx +++ b/src/components/main/container/PostContainer/index.tsx @@ -35,9 +35,7 @@ const RecentPostSection = ({ if (hasStaleData) { const movedRight = offset > fetchedForOffset; - const staleSlice = movedRight - ? recentPosts.slice(1, size) - : recentPosts.slice(0, size - 1); + const staleSlice = movedRight ? recentPosts.slice(1, size) : recentPosts.slice(0, size - 1); const skeletonKey = movedRight ? "skeleton-right" : "skeleton-left"; return ( <> @@ -217,13 +215,8 @@ export const PostContainer = ({ popularPosts }: { popularPosts: IPost[] }) => { } }, [deviceType]); - const { - recentPosts, - isPending, - isFetching, - isPlaceholderData, - fetchedForOffset, - } = useRecentPosts(size, offset); + const { recentPosts, isPending, isFetching, isPlaceholderData, fetchedForOffset } = + useRecentPosts(size, offset); const isLoading = isPending || (isFetching && isPlaceholderData); diff --git a/src/components/main/container/ProjectsSection/CardSwap/index.tsx b/src/components/main/container/ProjectsSection/CardSwap/index.tsx index 80330bf..ca44c59 100644 --- a/src/components/main/container/ProjectsSection/CardSwap/index.tsx +++ b/src/components/main/container/ProjectsSection/CardSwap/index.tsx @@ -53,7 +53,7 @@ export const Card = forwardRef( className={`${styles.card} ${customClass ?? ""} ${className ?? ""}`.trim()} {...rest} /> - ), + ) ); Card.displayName = "Card"; @@ -64,12 +64,7 @@ interface Slot { zIndex: number; } -const makeSlot = ( - i: number, - distX: number, - distY: number, - total: number, -): Slot => ({ +const makeSlot = (i: number, distX: number, distY: number, total: number): Slot => ({ x: i * distX, y: -i * distY, z: -i * distX * 1.5, @@ -129,10 +124,8 @@ export const CardSwap: React.FC = ({ const config = useMemo( () => animationConfig ?? - (easing === "elastic" - ? DEFAULT_ANIMATION.elastic - : DEFAULT_ANIMATION.linear), - [animationConfig, easing], + (easing === "elastic" ? DEFAULT_ANIMATION.elastic : DEFAULT_ANIMATION.linear), + [animationConfig, easing] ); const intervalMs = animationConfig && "intervalMs" in animationConfig @@ -142,13 +135,10 @@ export const CardSwap: React.FC = ({ ? DEFAULT_ANIMATION_WITH_INTERVAL.elastic.intervalMs : DEFAULT_ANIMATION_WITH_INTERVAL.linear.intervalMs)); - const childArr = useMemo( - () => Children.toArray(children) as ReactElement[], - [children], - ); + const childArr = useMemo(() => Children.toArray(children) as ReactElement[], [children]); const refs = useMemo[]>( () => childArr.map(() => React.createRef()), - [childArr], + [childArr] ); const order = useRef(Array.from({ length: childArr.length }, (_, i) => i)); @@ -159,11 +149,7 @@ export const CardSwap: React.FC = ({ useEffect(() => { const total = refs.length; refs.forEach((r, i) => - placeNow( - r.current!, - makeSlot(i, cardDistance, verticalDistance, total), - skewAmount, - ), + placeNow(r.current!, makeSlot(i, cardDistance, verticalDistance, total), skewAmount) ); const swap = () => { @@ -194,23 +180,18 @@ export const CardSwap: React.FC = ({ duration: config.durMove, ease: config.ease, }, - `promote+=${i * 0.15}`, + `promote+=${i * 0.15}` ); }); - const backSlot = makeSlot( - refs.length - 1, - cardDistance, - verticalDistance, - refs.length, - ); + const backSlot = makeSlot(refs.length - 1, cardDistance, verticalDistance, refs.length); tl.addLabel("return", `promote+=${config.durMove * config.returnDelay}`); tl.call( () => { gsap.set(elFront, { zIndex: backSlot.zIndex }); }, undefined, - "return", + "return" ); tl.to( elFront, @@ -221,7 +202,7 @@ export const CardSwap: React.FC = ({ duration: config.durReturn, ease: config.ease, }, - "return", + "return" ); tl.call(() => { @@ -251,16 +232,7 @@ export const CardSwap: React.FC = ({ }; } return () => clearInterval(intervalRef.current); - }, [ - cardDistance, - verticalDistance, - intervalMs, - pauseOnHover, - skewAmount, - easing, - config, - refs, - ]); + }, [cardDistance, verticalDistance, intervalMs, pauseOnHover, skewAmount, easing, config, refs]); const rendered = childArr.map((child, i) => isValidElement(child) @@ -281,15 +253,11 @@ export const CardSwap: React.FC = ({ onCardClick?.(i); }, } as CardProps & React.RefAttributes) - : child, + : child ); return ( -
    +
    {rendered}
    ); diff --git a/src/components/main/container/index.ts b/src/components/main/container/index.ts index d496f8f..d9addbf 100644 --- a/src/components/main/container/index.ts +++ b/src/components/main/container/index.ts @@ -4,10 +4,4 @@ import { PostContainer } from "@/components/main/container/PostContainer"; import { ProjectsSection } from "@/components/main/container/ProjectsSection"; import { SideContainer } from "@/components/main/container/SideContainer"; -export { - HeaderContainer, - InfoContainer, - PostContainer, - ProjectsSection, - SideContainer, -}; +export { HeaderContainer, InfoContainer, PostContainer, ProjectsSection, SideContainer }; diff --git a/src/components/posts/GenerateMarkdownFromPrompt/index.tsx b/src/components/posts/GenerateMarkdownFromPrompt/index.tsx index 9f8d928..60e9a50 100644 --- a/src/components/posts/GenerateMarkdownFromPrompt/index.tsx +++ b/src/components/posts/GenerateMarkdownFromPrompt/index.tsx @@ -37,9 +37,7 @@ export default function GenerateMarkdownFromPrompt() { setIsOpen(false); alert(t("posts.generateMarkdownSuccess")); } catch (err) { - setError( - err instanceof Error ? err.message : t("posts.generateMarkdownError") - ); + setError(err instanceof Error ? err.message : t("posts.generateMarkdownError")); } finally { setLoading(false); } @@ -113,9 +111,7 @@ export default function GenerateMarkdownFromPrompt() { className={styles.submit} disabled={loading || !prompt.trim()} > - {loading - ? t("posts.generateMarkdownLoading") - : t("posts.generateMarkdownSubmit")} + {loading ? t("posts.generateMarkdownLoading") : t("posts.generateMarkdownSubmit")}
    diff --git a/src/components/posts/GenerateMarkdownFromPrompt/styles.module.scss b/src/components/posts/GenerateMarkdownFromPrompt/styles.module.scss index de6c1cc..6e37b88 100644 --- a/src/components/posts/GenerateMarkdownFromPrompt/styles.module.scss +++ b/src/components/posts/GenerateMarkdownFromPrompt/styles.module.scss @@ -8,7 +8,10 @@ border-radius: 6px; cursor: pointer; white-space: nowrap; - transition: color 0.15s, background-color 0.15s, border-color 0.15s; + transition: + color 0.15s, + background-color 0.15s, + border-color 0.15s; &:hover:not(:disabled) { color: var(--text-color); diff --git a/src/components/posts/TagManager/index.tsx b/src/components/posts/TagManager/index.tsx index a50a419..6dc65c0 100644 --- a/src/components/posts/TagManager/index.tsx +++ b/src/components/posts/TagManager/index.tsx @@ -31,9 +31,7 @@ export const TagManager = () => { setFormData({ name: "", color: "#000000" }); setIsCreating(false); } catch (error) { - alert( - error instanceof Error ? error.message : t("posts.errorCreateTag") - ); + alert(error instanceof Error ? error.message : t("posts.errorCreateTag")); } }; @@ -51,9 +49,7 @@ export const TagManager = () => { setFormData({ name: "", color: "#000000" }); setIsEditing(null); } catch (error) { - alert( - error instanceof Error ? error.message : t("posts.errorEditTag") - ); + alert(error instanceof Error ? error.message : t("posts.errorEditTag")); } }; @@ -62,9 +58,7 @@ export const TagManager = () => { try { await removeTag.mutateAsync(id); } catch (error) { - alert( - error instanceof Error ? error.message : t("posts.errorDeleteTag") - ); + alert(error instanceof Error ? error.message : t("posts.errorDeleteTag")); } }; @@ -117,9 +111,7 @@ export const TagManager = () => { - setFormData({ ...formData, color: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, color: e.target.value })} className={styles.color_input} />
    @@ -141,24 +133,17 @@ export const TagManager = () => { - setFormData({ ...formData, name: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, name: e.target.value })} className={styles.input} /> - setFormData({ ...formData, color: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, color: e.target.value })} className={styles.color_input} />
    - -
    diff --git a/src/components/posts/TagManagerModal/index.tsx b/src/components/posts/TagManagerModal/index.tsx index 2c02d22..ebc2599 100644 --- a/src/components/posts/TagManagerModal/index.tsx +++ b/src/components/posts/TagManagerModal/index.tsx @@ -12,11 +12,7 @@ interface TagManagerModalProps { onTagsUpdated?: () => void; } -export const TagManagerModal = ({ - isOpen, - onClose, - onTagsUpdated, -}: TagManagerModalProps) => { +export const TagManagerModal = ({ isOpen, onClose, onTagsUpdated }: TagManagerModalProps) => { const { t } = useTranslation(); const user = useAppSelector((state) => state.user); const [isEditing, setIsEditing] = useState(null); @@ -47,9 +43,7 @@ export const TagManagerModal = ({ setIsCreating(false); onSuccess(); } catch (error) { - alert( - error instanceof Error ? error.message : t("posts.errorCreateTag") - ); + alert(error instanceof Error ? error.message : t("posts.errorCreateTag")); } }; @@ -68,9 +62,7 @@ export const TagManagerModal = ({ setIsEditing(null); onSuccess(); } catch (error) { - alert( - error instanceof Error ? error.message : t("posts.errorEditTag") - ); + alert(error instanceof Error ? error.message : t("posts.errorEditTag")); } }; @@ -80,9 +72,7 @@ export const TagManagerModal = ({ await removeTag.mutateAsync(id); onSuccess(); } catch (error) { - alert( - error instanceof Error ? error.message : t("posts.errorDeleteTag") - ); + alert(error instanceof Error ? error.message : t("posts.errorDeleteTag")); } }; @@ -119,9 +109,7 @@ export const TagManagerModal = ({
    {!isAdmin ? ( -
    - {t("posts.adminOnlyTagManage")} -
    +
    {t("posts.adminOnlyTagManage")}
    ) : isLoading ? (
    {t("posts.loadingTags")}
    ) : ( @@ -138,24 +126,17 @@ export const TagManagerModal = ({ type="text" placeholder={t("posts.tagNamePlaceholder")} value={formData.name} - onChange={(e) => - setFormData({ ...formData, name: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, name: e.target.value })} className={styles.input} /> - setFormData({ ...formData, color: e.target.value }) - } + onChange={(e) => setFormData({ ...formData, color: e.target.value })} className={styles.color_input} />
    - - -
    + setFormData({ ...formData, name: e.target.value })} + className={styles.input} + /> + + setFormData({ + ...formData, + color: e.target.value, + }) + } + className={styles.color_input} + /> +
    + + +
    ) : ( <> - + {tag.name}
    -
    - - {comment.userName || comment.userEmail} - + {comment.userName || comment.userEmail} {formatRelativeTime(createdAt)} {isEdited && ` ${t("posts.editedLabel")}`} diff --git a/src/components/posts/[id]/Comments/index.tsx b/src/components/posts/[id]/Comments/index.tsx index 3768da6..2c25e5b 100644 --- a/src/components/posts/[id]/Comments/index.tsx +++ b/src/components/posts/[id]/Comments/index.tsx @@ -6,12 +6,7 @@ import { CommentItem } from "./CommentItem"; import { CommentForm } from "./CommentForm"; import styles from "./styles.module.scss"; import { useTranslation } from "@/hooks/useTranslation"; -import { - useComments, - usePostComment, - usePutComment, - useDeleteComment, -} from "@/hooks/useComments"; +import { useComments, usePostComment, usePutComment, useDeleteComment } from "@/hooks/useComments"; interface CommentsProps { postIndex: number; @@ -101,9 +96,7 @@ export const Comments = ({ postIndex }: CommentsProps) => {
    {comments.length === 0 ? ( -
    - {t("posts.noCommentsYet")} -
    +
    {t("posts.noCommentsYet")}
    ) : ( <> {comments.map((comment) => ( diff --git a/src/components/posts/[id]/PostLikeButton/index.tsx b/src/components/posts/[id]/PostLikeButton/index.tsx index b83298f..e3736dd 100644 --- a/src/components/posts/[id]/PostLikeButton/index.tsx +++ b/src/components/posts/[id]/PostLikeButton/index.tsx @@ -19,7 +19,7 @@ export const PostLikeButton = ({ post, onLikeToggle }: PostLikeButtonProps) => { const { t } = useTranslation(); const user = useAppSelector((state) => state.user); const [isPending, startTransition] = useTransition(); - + // likes가 배열인지 확인하고 파싱 const parseLikes = (likes: any): string[] => { if (Array.isArray(likes)) { @@ -37,9 +37,7 @@ export const PostLikeButton = ({ post, onLikeToggle }: PostLikeButtonProps) => { }; const likesArray = parseLikes(post.likes); - const [isLiked, setIsLiked] = useState( - user.id ? likesArray.includes(user.id) : false - ); + const [isLiked, setIsLiked] = useState(user.id ? likesArray.includes(user.id) : false); const [likeCount, setLikeCount] = useState(likesArray.length); const [isLikeModalOpen, setIsLikeModalOpen] = useState(false); @@ -58,11 +56,7 @@ export const PostLikeButton = ({ post, onLikeToggle }: PostLikeButtonProps) => { onLikeToggle?.(newIsLiked, newCount); } catch (error) { console.error("Failed to toggle like:", error); - alert( - error instanceof Error - ? error.message - : t("posts.errorLike") - ); + alert(error instanceof Error ? error.message : t("posts.errorLike")); } })(); }); @@ -84,10 +78,18 @@ export const PostLikeButton = ({ post, onLikeToggle }: PostLikeButtonProps) => {
    setIsLikeModalOpen(false)}>
    e.stopPropagation()}>

    {t("posts.likeAvailableWhenLogin")}

    - setIsLikeModalOpen(false)}> + setIsLikeModalOpen(false)} + > {t("common.goToLogin")} -
    diff --git a/src/components/posts/[id]/PostLikeButton/styles.module.scss b/src/components/posts/[id]/PostLikeButton/styles.module.scss index 90f8a1e..d2ba3e7 100644 --- a/src/components/posts/[id]/PostLikeButton/styles.module.scss +++ b/src/components/posts/[id]/PostLikeButton/styles.module.scss @@ -30,7 +30,7 @@ .liked { color: var(--error-color); - + .like_button:active & { animation: heartBeat 0.3s ease-in-out; } diff --git a/src/components/posts/[id]/PostNavigation/index.tsx b/src/components/posts/[id]/PostNavigation/index.tsx index b1f6a59..b5e7521 100644 --- a/src/components/posts/[id]/PostNavigation/index.tsx +++ b/src/components/posts/[id]/PostNavigation/index.tsx @@ -10,28 +10,19 @@ interface PostNavigationProps { nextPost?: IPost; } -const PostNavigation: React.FC = ({ - previousPost, - nextPost, -}) => { +const PostNavigation: React.FC = ({ previousPost, nextPost }) => { const { t } = useTranslation(); return (
    {previousPost ? ( - + {previousPost.title} ) : ( {t("posts.noPrevPost")} )} {nextPost ? ( - + {nextPost.title} ) : ( diff --git a/src/components/posts/[id]/ThumbnailGenerateButton/index.tsx b/src/components/posts/[id]/ThumbnailGenerateButton/index.tsx index 1853139..baeff09 100644 --- a/src/components/posts/[id]/ThumbnailGenerateButton/index.tsx +++ b/src/components/posts/[id]/ThumbnailGenerateButton/index.tsx @@ -49,9 +49,7 @@ export default function ThumbnailGenerateButton({ postIndex }: IProps) { setIsOpen(false); alert(t("posts.generateThumbnailSuccess")); } catch (err) { - setError( - err instanceof Error ? err.message : t("posts.generateThumbnailError") - ); + setError(err instanceof Error ? err.message : t("posts.generateThumbnailError")); } finally { setLoading(false); } @@ -121,11 +119,7 @@ export default function ThumbnailGenerateButton({ postIndex }: IProps) { > {t("common.cancel")} -