Skip to content

Feature/prompt crud#102#127

Open
muzi55 wants to merge 19 commits intodevfrom
feature/prompt-crud#102
Open

Feature/prompt crud#102#127
muzi55 wants to merge 19 commits intodevfrom
feature/prompt-crud#102

Conversation

@muzi55
Copy link
Copy Markdown
Contributor

@muzi55 muzi55 commented Mar 9, 2026

🛠️ 변경 사항

무엇을 변경했는지 간결하게 설명해주세요.

  • prompt 부분 api가 명확하지 않아서 일단 백엔드 코드에서 dto추려서 guide.md 파일로 작업을 했어요
  • 이부분은 후에 분명히 문제가 생기지만, 일단 ui를 보여줘야하기떄문에 일단 먼저 올릴게요

📸 스크린샷 (선택)

UI 변경이 있는 경우 스크린샷을 첨부해주세요.

스크린샷 2026-03-09 오후 4 40 01

✅ 체크리스트

  • 빌드 에러가 발생하지 않는가?
  • 정해진 디자인 시스템(Color, Font)을 준수하였는가?
  • 불필요한 console.log는 제거하였는가?

🔗 연결된 이슈

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 프롬프트 조회·작성·수정·삭제 워크플로우 및 작성 편집 UI 추가
    • 댓글·답글 작성, 댓글 리스트 정렬 및 좋아요/복사/신고 기능 제공
    • 임시저장/작성 네비게이션 및 로딩 스켈레톤 UX 개선
  • Documentation

    • 프롬프트 관련 API 명세서 추가
  • Refactor

    • 라우팅 재구성: 쓰기/수정 경로를 프롬프트 섹션으로 이동
  • Chores

    • 폼 검증 라이브러리 및 유틸 추가, 개발 환경 gitignore 업데이트

@muzi55 muzi55 linked an issue Mar 9, 2026 that may be closed by this pull request
1 task
@muzi55 muzi55 self-assigned this Mar 9, 2026
@muzi55 muzi55 added the feature 새로운 기능 구현 label Mar 9, 2026
@muzi55 muzi55 added this to prog-front Mar 9, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

이 PR은 프롬프트(게시글)와 댓글 기능을 새로 추가하거나 확장합니다. 주요 변경사항은 프롬프트 CRUD API와 클라이언트 API 래퍼(promptApi, promptQueries), 상세 페이지 및 작성/수정 폼(PromptForm, PromptDetailContent 등), 댓글 컴포넌트(CommentForm, CommentList, CommentItem), 낙관적 좋아요 토글, 폼 검증(boardSchema·Zod), 모의 API 핸들러 확장(mocks), 그리고 관련 유틸(stripHtml, formatCommentDate) 및 라우트 변경(/prompt/*)입니다. 또한 개발 편의 설정(.vscode gitignore, .vscode/mcp.json)과 의존성(zod, react-hook-form 등)이 추가되었습니다.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant UI as PromptForm (클라이언트)
    participant RHF as react-hook-form / Zod
    participant Query as React Query (mutation)
    participant API as promptApi (서버/모의)
    participant Cache as React Query Cache
    participant Toast as Toast Notification

    User->>UI: 작성 후 제출
    UI->>RHF: 입력 검증 (Zod)
    RHF-->>UI: 검증 결과
    alt 검증 성공
        UI->>Query: createPrompt mutation 실행
        Query->>API: POST /prompts
        API-->>Query: 생성 응답 (promptId)
        Query->>Cache: invalidate prompt lists
        Cache-->>UI: 캐시 갱신 완료
        UI->>Toast: 성공 토스트 표시
        UI->>User: 상세 페이지로 이동
    else 검증 실패
        UI->>User: 필드 오류 표시
    end
Loading
sequenceDiagram
    participant User as 사용자
    participant Page as PromptDetailPage
    participant Query as React Query (suspense)
    participant API as promptApi (서버/모의)
    participant Post as PostDetail 컴포넌트
    participant CList as CommentList
    participant CForm as CommentForm
    participant Cache as React Query Cache
    participant Toast as Toast Notification

    Page->>Query: getPromptDetail(id) 요청
    Query->>API: GET /prompts/:id
    API-->>Query: prompt 데이터 반환
    Query-->>Page: 데이터 제공
    Page->>Post: prompt 전달 (렌더)
    Page->>CList: promptId 전달
    CList->>Query: getComments(promptId)
    Query->>API: GET /api/v1/comment/:promptId
    API-->>Query: 댓글 배열 반환
    Query-->>CList: 댓글 제공 (정렬 클라이언트 처리)
    User->>CForm: 댓글 입력 및 제출
    CForm->>Query: createComment mutation
    Query->>API: POST /api/v1/comment/:promptId
    API-->>Query: 생성 응답
    Query->>Cache: invalidate comments(promptId)
    Cache-->>CList: 댓글 목록 갱신
    CList->>User: 새 댓글 표시
    alt 에러
        Query->>CForm: 에러 반환
        CForm->>Toast: 에러 토스트 표시
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60분

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive PR 제목이 '게시글 CRUD' 기능을 명확하게 나타내지 못하고 있습니다. 'Feature/prompt crud#102'는 지라 티켓 번호를 포함하고 있어 가독성이 낮습니다. PR 제목을 'feat: 프롬프트 CRUD 기능 구현'과 같이 변경하여 이 변경사항의 핵심을 명확하게 표현해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 템플릿을 대부분 따르고 있으나 구체적인 변경 사항 목록이 불충분합니다. 스크린샷은 포함되어 있으나 체크리스트가 미완료 상태입니다.
Linked Issues check ✅ Passed PR 변경사항이 #102의 '게시글 CRUD 로직 작성' 요구사항을 충족합니다. 프롬프트 생성, 조회, 수정, 삭제 API와 UI 컴포넌트, 댓글 기능을 모두 구현했습니다.
Out of Scope Changes check ✅ Passed 대부분의 변경사항이 프롬프트 CRUD 범위 내이나, .gitignore, .vscode/mcp.json, 라우팅 일관성 수정 등 일부 부가적 변경이 포함되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/prompt-crud#102
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.

Add a .trivyignore file to your project to customize which findings Trivy reports.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mocks/handlers/prompt-list-handlers.ts (1)

128-128: 🛠️ Refactor suggestion | 🟠 Major

console.log를 제거해주세요.

PR 체크리스트에도 console.log 제거가 포함되어 있습니다. 디버그 로그는 커밋 전에 정리해주세요.

-console.log(`Prompt ${promptId} 좋아요 취소됨`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mocks/handlers/prompt-list-handlers.ts` at line 128, Remove the stray
console.log call that prints `Prompt ${promptId} 좋아요 취소됨` from
mocks/handlers/prompt-list-handlers.ts; locate the line referencing promptId in
the handler (the console.log within the unlike/unfavorite handler) and delete
it, or replace it with the project's standardized logger (e.g., use existing
processLogger or a logger.info/debug call) if persistent logging is required.
🧹 Nitpick comments (14)
package.json (1)

22-22: 검증 계약의 원본을 하나로 묶어두는 편이 안전합니다.

PR 설명처럼 현재는 백엔드 DTO가 guide.md로 분리돼 있는데, 여기서 Zod 스키마까지 추가되면 문서/프론트 검증/백엔드 DTO가 세 군데로 갈립니다. 카테고리 enum, 최대 길이, optional 필드 하나만 어긋나도 작성 폼은 통과하지만 API는 400을 내는 식의 drift가 바로 생겨요. 가능하면 서버 DTO에서 스키마를 생성하거나, shared contract 패키지/OpenAPI codegen처럼 계약의 단일 소스를 두는 쪽을 추천합니다.

Also applies to: 51-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 22, You currently have validation logic split between
guide.md, front-end Zod schemas and backend DTOs which risks drift; consolidate
the contract by generating Zod schemas from the server DTOs or moving DTOs + Zod
schemas into a shared package/OpenAPI codegen so form validation and API DTOs
share one source of truth; update package.json (the "@hookform/resolvers"
dependency and related front-end schema files) to consume the shared contract
(or the generated schemas) and remove duplicated schema copies referenced in
guide.md, and adjust import paths in form code to import the canonical Zod
schemas instead of local copies.
app/(afterLogin)/prompt/[id]/_components/post-detail.tsx (2)

95-102: 클립보드 복사 실패 시 사용자에게 피드백을 제공해주세요.

현재 클립보드 API 미지원 환경에서 에러가 발생하면 사용자에게 아무런 피드백 없이 무시됩니다. 사용자 경험을 위해 실패 토스트를 표시하는 것이 좋습니다.

💡 에러 처리 추가
 const handleCopy = useCallback(async () => {
   try {
     await navigator.clipboard.writeText(prompt.content);
     toasts.success("프롬프트가 복사되었습니다.");
   } catch {
-    // 클립보드 API 미지원 환경 대비
+    toasts.error("복사에 실패했습니다. 브라우저가 클립보드를 지원하지 않습니다.");
   }
 }, [prompt.content]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/[id]/_components/post-detail.tsx around lines 95 -
102, The handleCopy callback silently ignores failures; update its catch block
to show a failure toast so users get feedback when
navigator.clipboard.writeText(prompt.content) fails—use the existing toasts
utility (e.g., toasts.error("복사에 실패했습니다.") or a localized message) and
optionally log the error for debugging; ensure handleCopy still depends on
prompt.content.

107-111: userDesc 필드에도 fallback 값을 추가해주세요.

userIconuserName에는 ?? ""로 fallback이 적용되어 있지만, userDesc는 그대로 전달됩니다. undefined가 전달될 경우 렌더링 문제가 발생할 수 있습니다.

✨ 일관된 fallback 적용
 <AuthorProfileCard
   userIcon={prompt.userIcon ?? ""}
   userName={prompt.userName ?? ""}
-  userDesc={prompt.userDesc}
+  userDesc={prompt.userDesc ?? ""}
 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/[id]/_components/post-detail.tsx around lines 107 -
111, The AuthorProfileCard is being passed prompt.userDesc without a fallback
which can yield undefined rendering issues; update the JSX that renders
AuthorProfileCard to pass a safe fallback for the userDesc prop (e.g., use
prompt.userDesc ?? "" or another default string) alongside the existing
prompt.userIcon ?? "" and prompt.userName ?? "" so all three props consistently
have defaults.
app/(afterLogin)/prompt/board-schema.ts (1)

13-19: content 유효성 검사 순서와 불필요한 주석 정리가 필요합니다.

  1. 빈 HTML 태그 처리: min(1) 검사가 stripHtml 전에 실행되어 <p></p> 같은 빈 HTML이 통과할 수 있습니다.
  2. Line 19: 주석 처리된 코드는 제거해주세요.
♻️ 개선된 content 검증
 content: z
   .string()
-  .min(1, "내용은 필수입니다.")
-  .refine((val) => stripHtml(val).length <= MAX_BOARD_CONTENT_LENGTH, {
-    message: `내용은 최대 ${MAX_BOARD_CONTENT_LENGTH}자까지 입력 가능합니다.`,
-  }),
-// category:
+  .refine((val) => stripHtml(val).trim().length >= 1, {
+    message: "내용은 필수입니다.",
+  })
+  .refine((val) => stripHtml(val).length <= MAX_BOARD_CONTENT_LENGTH, {
+    message: `내용은 최대 ${MAX_BOARD_CONTENT_LENGTH}자까지 입력 가능합니다.`,
+  }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/board-schema.ts around lines 13 - 19, 현재 content 필드의
유효성 검사 순서가 잘못되어 z.string().min(1) 검사가 stripHtml보다 먼저 실행돼 "<p></p>" 같은 빈 HTML이
통과될 수 있으므로, content 검증 순서를 변경해 stripHtml로 HTML 제거 후 길이 검사(refine)로 빈 문자열/최대
길이(MAX_BOARD_CONTENT_LENGTH)를 확인하도록 수정하고, 관련 로직에서 사용하는 함수명(stripHtml)과
상수(MAX_BOARD_CONTENT_LENGTH)를 그대로 유지해 위치를 찾아 적용하세요; 또한 파일에 남아있는 주석 처리된 category
관련 코드(주석된 라인)는 제거하세요.
components/board/board.tsx (2)

54-58: 문자 수 카운터 UI 개선 제안

에러 상태일 때 문자 수를 빨간색으로 표시하면 사용자가 초과 여부를 더 직관적으로 인지할 수 있습니다.

✨ 에러 상태 시각화 추가
 <p className="text-xs text-gray-500">
-  {stripHtml(value).length} / {MAX_BOARD_CONTENT_LENGTH}
+  <span className={error ? "text-red-500 font-medium" : ""}>
+    {stripHtml(value).length}
+  </span>
+  {" / "}
+  {MAX_BOARD_CONTENT_LENGTH}
 </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/board/board.tsx` around lines 54 - 58, The character counter UI in
board.tsx currently always renders neutral styling; update the counter so when
stripHtml(value).length > MAX_BOARD_CONTENT_LENGTH the text uses an error style
(e.g., change the className to include a red text class) and otherwise keeps the
existing gray class. Locate the counter markup that renders
{stripHtml(value).length} / {MAX_BOARD_CONTENT_LENGTH} and conditionally apply
the red styling (or a CSS error class) based on the length check so the count
turns red when the limit is exceeded.

29-42: ...props spread의 타입 안전성과 prop 충돌 가능성을 검토해주세요.

BoardProps 인터페이스에 ...props에 대한 타입 정의가 없어 타입 안전성이 떨어집니다. 또한, react-hook-formController와 함께 사용 시 field 객체에 포함된 onChange, ref 등이 ...props로 전달되어 예기치 않은 동작을 유발할 수 있습니다.

Context snippet (write-board.tsx:105-115)에서 확인된 사용 패턴:

<Board value={field.value} setValue={field.onChange} error={...} />

현재 QuillBoard...props를 먼저 spread 후 명시적 props를 적용하므로 동작은 하지만, 의도를 명확히 하기 위해 타입을 정의하는 것이 좋습니다.

♻️ 타입 안전성 개선 제안
-export interface BoardProps {
+export interface BoardProps extends Omit<
+  React.ComponentProps<typeof QuillBoardDynamic>,
+  "value" | "setValue" | "placeholder"
+> {
   value: string;
   setValue: (value: string) => void;
   placeholder?: string;
   error?: boolean;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/board/board.tsx` around lines 29 - 42, Board currently accepts an
untyped ...props which is unsafe and can accidentally override handlers from
react-hook-form; update BoardProps to explicitly type/omit potential conflicts
by deriving remaining props from the underlying QuillBoard (or a DOM element)
and excluding controlled keys: e.g. change BoardProps to extend
Omit<React.ComponentPropsWithoutRef<typeof
QuillBoard>,'value'|'onChange'|'placeholder'|'error'> (or
Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'ref'>) and rename
...props to something like htmlProps, then spread htmlProps when rendering
QuillBoard (ensure you do not spread before explicitly passing
value/setValue/error) so onChange/ref from react-hook-form cannot be
unintentionally overridden.
mocks/handlers/prompt-list-handlers.ts (2)

259-267: DELETE 핸들러에서 사용되지 않는 promptId 변수가 있습니다.

promptId를 추출하지만 응답에서 사용하지 않습니다. 응답에 삭제된 ID를 포함하거나, 사용하지 않는다면 destructuring을 제거하세요.

✨ 응답에 ID 포함
 http.delete(`${BASE_URL}/prompts/:promptId`, ({ params }) => {
   const { promptId } = params;
   return HttpResponse.json({
     status: 200,
     success: true,
+    data: { promptId: Number(promptId) },
     message: "프롬프트가 삭제되었습니다.",
   });
 }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mocks/handlers/prompt-list-handlers.ts` around lines 259 - 267, The DELETE
handler defined with http.delete that currently destructures params to get
promptId but never uses it should either include the deleted ID in the response
or stop extracting it; update the handler (the http.delete callback and its
params destructuring) so the JSON response contains the promptId (e.g., include
promptId in the message or a dedicated field like deletedId) or remove the {
promptId } destructuring if you intentionally don't return it, ensuring the
variable is not unused.

114-137: 좋아요 엔드포인트 핸들러가 중복되어 있습니다.

두 가지 다른 경로로 좋아요 기능이 구현되어 있습니다:

  • Lines 114-137: POST/DELETE /prompts/like/:promptId
  • Lines 317-325: POST /prompts/:promptId/like

API 가이드 문서(prompt.guide.md)에 따르면 POST /prompts/{promptId}/like가 올바른 경로입니다. 중복 핸들러를 정리하고 일관된 엔드포인트를 사용해주세요.

🧹 중복 핸들러 제거

Lines 114-137의 /prompts/like/:promptId 핸들러를 제거하고, Line 317-325의 /prompts/:promptId/like 핸들러만 유지하세요.

Also applies to: 317-325

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mocks/handlers/prompt-list-handlers.ts` around lines 114 - 137, Remove the
duplicate like handlers implemented as
http.post(`${BASE_URL}/prompts/like/:promptId`, ...) and
http.delete(`${BASE_URL}/prompts/like/:promptId`, ...) and keep only the
canonical handler http.post(`${BASE_URL}/prompts/:promptId/like`, ...) as
specified in the API guide; delete the POST/DELETE blocks for
`/prompts/like/:promptId`, ensure any test/mocking references point to
`/prompts/:promptId/like`, and remove the redundant console.log in the deleted
handlers if it was the only side effect.
queries/options/prompt-query.ts (1)

81-91: 파라미터 타입 불일치

createReply, updateComment, deleteComment의 파라미터 타입이 string으로 좁혀져 있지만, promptApi의 해당 메서드는 string | number를 받습니다. 타입 일관성을 위해 동일한 타입을 사용하거나, 의도적인 제한이라면 주석을 추가하세요.

♻️ 타입 일관성 개선
-  createReply: (
-    promptId: string,
-    commentId: string,
-    data: PromptCommentRequest
-  ) => promptApi.createReply(promptId, commentId, data).then((res) => res.data),
-  updateComment: (commentId: string, data: PromptCommentRequest) =>
-    promptApi.updateComment(commentId, data).then((res) => res.data),
-  deleteComment: (commentId: string) =>
-    promptApi.deleteComment(commentId).then((res) => res.data),
+  createReply: (
+    promptId: string | number,
+    commentId: string | number,
+    data: PromptCommentRequest
+  ) => promptApi.createReply(promptId, commentId, data).then((res) => res.data),
+  updateComment: (commentId: string | number, data: PromptCommentRequest) =>
+    promptApi.updateComment(commentId, data).then((res) => res.data),
+  deleteComment: (commentId: string | number) =>
+    promptApi.deleteComment(commentId).then((res) => res.data),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@queries/options/prompt-query.ts` around lines 81 - 91, The parameter types
for createReply, updateComment, and deleteComment are narrowed to string but
promptApi methods accept string | number; update the function signatures for
createReply, updateComment, and deleteComment to use promptId/commentId: string
| number (matching createComment and promptApi), or if the string-only
restriction is intentional, add a clear comment on each method (createReply,
updateComment, deleteComment) explaining why numbers are disallowed; ensure the
types align with promptApi.createReply, promptApi.updateComment, and
promptApi.deleteComment to maintain type consistency.
app/(afterLogin)/prompt/hook/use-prompt-query.tsx (1)

8-23: 훅 네이밍 개선 및 에러 처리 보완 제안

  1. usePromptQuery라는 이름은 범용적으로 들리지만 실제로는 생성(create) 뮤테이션만 담당합니다. useCreatePromptusePromptCreateMutation으로 명확하게 표현하는 것이 좋습니다.

  2. onError에서 console.error만 호출하고 사용자에게 피드백이 없습니다. UX 개선을 위해 에러 토스트를 추가하세요.

♻️ 개선 제안
-export default function usePromptQuery() {
+export default function useCreatePrompt() {
   const queryClient = useQueryClient();

   return useMutation({
     mutationFn: promptQueries.create,
     onSuccess: () => {
       queryClient.invalidateQueries({
         queryKey: promptQueries.lists(),
       });
       toasts.success("프롬프트가 성공적으로 생성되었습니다!");
     },
     onError: (error) => {
-      // 프롬프트 생성 실패 시 에러 처리
-      console.error("프롬프트 생성 실패:", error);
+      console.error("프롬프트 생성 실패:", error);
+      toasts.error("프롬프트 생성에 실패했습니다. 다시 시도해주세요.");
     },
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/hook/use-prompt-query.tsx around lines 8 - 23,
Rename the hook usePromptQuery to a name that reflects it only performs creation
(e.g., useCreatePrompt or usePromptCreateMutation) and update the default export
accordingly; inside the mutation returned (currently using mutationFn:
promptQueries.create) replace the onError body that only console.errors with
calling your toast error helper (e.g., toasts.error) including a brief
user-facing message and any safe error detail, and keep the existing
queryClient.invalidateQueries call in onSuccess and the toasts.success call
unchanged; ensure any callers/imports of usePromptQuery are updated to the new
hook name.
app/(afterLogin)/prompt/[id]/_components/comment-list.tsx (1)

50-70: getRepliesForComment 함수 최적화 제안

getRepliesForComment가 매 렌더링마다 재생성되고, sortedComments useMemo 내에서 반복 호출됩니다. 댓글이 많아지면 성능에 영향을 줄 수 있습니다. useCallback으로 메모이제이션하거나, replies를 Map으로 미리 그룹화하는 것을 고려해보세요.

♻️ Map 기반 그룹화 최적화
+  // 대댓글을 parentId 기준으로 미리 그룹화
+  const repliesByParentId = useMemo(() => {
+    const map = new Map<string, PromptCommentResponse[]>();
+    replies.forEach((r) => {
+      if (r.parentId) {
+        const existing = map.get(r.parentId) || [];
+        existing.push(r);
+        map.set(r.parentId, existing);
+      }
+    });
+    return map;
+  }, [replies]);
+
   /** 특정 댓글의 대댓글 목록 반환 */
-  const getRepliesForComment = (commentId: string): PromptCommentResponse[] =>
-    replies.filter((r) => r.parentId === commentId);
+  const getRepliesForComment = (commentId: string): PromptCommentResponse[] =>
+    repliesByParentId.get(commentId) || [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/[id]/_components/comment-list.tsx around lines 50 -
70, getRepliesForComment is recomputed and called repeatedly inside
sortedComments causing O(n*m) work; memoize it or pre-group replies into a Map
to avoid repeated filtering. Update the component to derive a repliesByParent
map via useMemo (dependent on replies) and change getRepliesForComment to return
repliesByParent.get(commentId) || [] (or wrap getRepliesForComment with
useCallback that reads the memoized map); then update sortedComments to use that
map-backed lookup instead of filtering the replies array each time.
app/(afterLogin)/prompt/[id]/_components/comment-form.tsx (1)

42-58: 불필요한 try-catch 및 에러 피드백 누락

  1. onSuccess 내부의 try-catch는 불필요합니다. setCommentinvalidateQueries는 동기적으로 예외를 던지지 않습니다.

  2. onError가 비어있어 사용자가 실패 시 아무 피드백을 받지 못합니다.

♻️ 개선 제안
     onSuccess: () => {
-      try {
-        setComment("");
-        // 댓글 목록 캐시 무효화
-        queryClient.invalidateQueries({
-          queryKey: promptQueries.comments(promptId).queryKey,
-        });
-        toasts.success("댓글이 등록되었습니다.");
-        onSuccess?.();
-      } catch {
-        // 캐시 무효화 실패 시 무시
-      }
+      setComment("");
+      queryClient.invalidateQueries({
+        queryKey: promptQueries.comments(promptId).queryKey,
+      });
+      toasts.success("댓글이 등록되었습니다.");
+      onSuccess?.();
     },
     onError: () => {
-      // 댓글 작성 실패 시 사용자에게 알림 없이 무시 (추후 토스트 추가 가능)
+      toasts.error("댓글 등록에 실패했습니다. 다시 시도해주세요.");
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/[id]/_components/comment-form.tsx around lines 42 -
58, Remove the unnecessary try-catch inside the mutation's onSuccess: directly
call setComment("") and queryClient.invalidateQueries({ queryKey:
promptQueries.comments(promptId).queryKey }) then call toasts.success("댓글이
등록되었습니다.") and onSuccess?.(); also implement onError to surface failures to the
user (e.g., call toasts.error with a helpful message) so users get feedback when
the mutation fails; keep references to setComment,
queryClient.invalidateQueries, promptQueries.comments(promptId).queryKey,
toasts.success, onSuccess, and onError when making these changes.
app/(afterLogin)/prompt/[id]/_components/comment-item.tsx (2)

79-106: 좋아요/신고 버튼 기능 미구현

좋아요와 신고 버튼이 UI만 존재하고 onClick 핸들러가 없습니다. 의도적인 단계적 구현이라면 TODO 주석을 추가하여 추후 작업을 명시하는 것이 좋습니다.

이 기능들의 구현이 필요하시면 말씀해주세요. 좋아요 토글 뮤테이션과 신고 모달 로직을 작성해 드릴 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/[id]/_components/comment-item.tsx around lines 79 -
106, The like and report buttons in CommentItem currently have no onClick
handlers; add explicit handlers (e.g., handleLikeToggle and handleReportClick)
to the like button and report button respectively or, if deferred, add TODO
comments above the JSX to indicate planned work. Implement handleLikeToggle to
call the like-toggle mutation/dispatch and update optimistic UI state in
CommentItem, and implement handleReportClick to open the report modal or
navigate to the report flow (invoke the modal open function or router). Ensure
the button elements (the ThumbsUp and Siren buttons) include the onClick prop
wired to these handlers and proper accessible labels/disabled handling while the
operations are in-flight.

37-43: 프로필 아이콘 하드코딩

ProfIconsrcnull로 하드코딩되어 있습니다. 댓글 작성자의 아이콘 정보가 API 응답에 포함된다면 이를 활용해야 합니다. PromptCommentResponse 타입에 사용자 아이콘 필드가 없다면 백엔드와 협의하여 추가를 고려하세요.

♻️ 향후 개선 방향
 <ProfIcon
-  src={null}
+  src={comment.userIcon ?? null}  // PromptCommentResponse에 userIcon 필드 추가 필요
   width={60}
   height={60}
   alt={comment.nickName}
   fallback={comment.nickName}
 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/[id]/_components/comment-item.tsx around lines 37 -
43, ProfIcon is being rendered with src={null} which hardcodes the image source;
update the ProfIcon usage in comment-item.tsx to pass the actual avatar/icon
field from the comment object (e.g., comment.avatar, comment.userIcon or
comment.user?.icon depending on the shape returned by PromptCommentResponse) and
keep fallback={comment.nickName} for missing images; if PromptCommentResponse
lacks an icon/avatar property, coordinate with backend to add a clear field
(name it avatar or icon) and then wire that property into ProfIcon's src.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.vscode/mcp.json:
- Around line 3-5: The tracked .vscode/mcp.json contains a local MCP endpoint
("mcp-figma" with "url": "http://127.0.0.1:3845/mcp") and needs either
documentation or exclusion: either add a setup section to README.md or
CONTRIBUTING.md describing how to run the local MCP server (port 3845 and the
exact start command) so teammates can reproduce the endpoint, or treat the file
as per-developer config by adding .vscode/mcp.json to .gitignore and committing
a template like .vscode/mcp.json.example or docs/mcp.md that shows the
"mcp-figma" key and URL format for users to copy.

In `@app/`(afterLogin)/prompt/[id]/edit/page.tsx:
- Around line 1-2: The edit page currently renders a placeholder string and
exposes ROUTES.prompt.EDIT without functionality; guard the route until
implementation by updating the page() component to call Next.js routing helpers
(e.g., notFound() or redirect(ROUTES.prompt.SOME_SAFE_ROUTE)) instead of
returning the placeholder, so users cannot access the incomplete UI; locate the
default export function page in this file and add the appropriate early
return/guard using notFound() or redirect() until the full edit UI is
implemented.

In `@app/`(afterLogin)/prompt/board-navigate.tsx:
- Around line 20-22: The button element using onClick={handleGoBack} with
<ArrowLeftIcon /> lacks accessible name and explicit type; update that button to
include aria-label="뒤로가기" (or an appropriate localized label) and add
type="button" to prevent accidental form submits, ensuring the clickable icon is
announced properly by screen readers and safe inside forms.
- Around line 26-28: The "임시저장된 글(1)" BaseButton in board-navigate.tsx is
hardcoded and non-functional; change it so that it either hides or renders
disabled when there is no draft feature/count, and when drafts exist wire its
onClick to the real navigation handler (e.g., call the existing
router/navigation function used elsewhere in this component) and replace the
hardcoded "(1)" with a dynamic draftCount value from props/state (or a selector)
so the label and click behavior reflect actual data; use BaseButton's disabled
prop when showing but not actionable, or conditionally render nothing when the
feature is unavailable.

In `@app/`(afterLogin)/prompt/layout.tsx:
- Around line 14-16: BoardNavigate in board-navigate.tsx is always rendering
"게시글 작성하기" and the temp-save button which is inappropriate on the detail page;
update BoardNavigate to accept a mode prop (e.g., mode?: "write" | "detail" and
optional title?) and branch rendering so that when mode === "write" it shows the
"게시글 작성하기" title and the 임시저장된 글 button, and when mode === "detail" it shows a
detail title (use passed title or fallback) and hides the temp-save button; then
update where BoardNavigate is used in layout.tsx (or pass mode from prompt/write
and prompt/[id] pages) so the write page passes mode="write" and the detail page
passes mode="detail".

In `@app/`(afterLogin)/prompt/query-option.ts:
- Around line 6-14: The mutationFn in prompt/query-option.ts currently has its
API call commented out making the try block effectively empty and the catch
unreachable, and it returns undefined; update mutationFn (the async mutationFn)
to either implement the real API call (e.g., call createPrompt and return its
response) or provide a clear temporary implementation that returns a resolved
value or throws a descriptive error, and ensure the catch block can actually
catch thrown errors (or replace it with a TODO comment and a thrown Error to
make intent explicit); adjust or remove the commented-out lines so mutationFn
reliably returns a value or throws so callers don't receive undefined.
- Line 18: The code calls queryClient.invalidateQueries() with no key—this
invalidates all queries; import the specific query key exported from
queries/options/prompt-query.ts (the exported query key constant/function) and
call queryClient.invalidateQueries(<thatQueryKey>) instead; update the three
occurrences of queryClient.invalidateQueries() in prompt/query-option.ts (around
the current calls at lines ~18, ~40, ~62) to use the imported query key so only
the prompt options query is invalidated.
- Around line 15-22: The onSuccess callbacks call useQueryClient() directly
(violating Rules of Hooks); move the useQueryClient() call to the top level
(component or custom hook) and pass the resulting queryClient into the mutation
options or return a mutationOptions factory that accepts queryClient; update the
places referencing mutationOptions and onSuccess to use the closed-over
queryClient (or construct the mutation inside the component), ensuring unique
identifiers like useQueryClient, onSuccess, and mutationOptions are used to
locate and fix the code.

In `@app/`(afterLogin)/prompt/write/write-board.tsx:
- Around line 32-36: The form currently sends the UI category strings (e.g.,
"general" | "feedback" | "question") directly as PromptCreateRequest.categoryId
in handleSubmit, but the backend expects domain enums like "BACKEND" |
"FRONTEND" | "AI" | "ETC"; add an explicit mapping before building the
PromptCreateRequest (inside handleSubmit) that converts BoardFormData.category
values to the corresponding backend enum, or change the form option value(s) to
those backend enum values so PromptCreateRequest.categoryId matches the API
contract exactly; update the same mapping for the identical submission code path
at the other occurrence (lines ~85-89) to keep behavior consistent.
- Around line 126-132: The "임시저장" BaseButton currently has no behavior and must
not be exposed as a no-op; either hide it or render it disabled with a clear
label and tooltip, or implement a real save flow. Add a handler (e.g.,
handleSaveDraft / saveDraft) that performs the draft save, wires the button's
onClick to that handler, and surface success/error feedback (e.g., showToast or
setStatus state) and disabled/loading states on BaseButton while saving; if you
choose to hide the feature instead, conditionally omit rendering the BaseButton
or set disabled and an accessible description (aria-disabled/aria-label) so
users aren’t misled. Ensure BaseButton's props (onClick, disabled, aria-*) and
any save functions are added/used consistently where the current button is
rendered.
- Around line 147-157: CLSBox currently renders the text prop inside a <p>,
which can create invalid nested <p><p>...</p></p> when callers already pass a
<p>; update CLSBox (the function CLSBox) to avoid nesting by changing the
wrapper for the text from a <p> to a neutral element like <div> or <span>, or
restrict the text prop to a string and render it as plain text; ensure the
returned JSX uses the new element (e.g., <div className="absolute bottom-1
left-0"> or <span ...>) so callers can safely pass block or inline elements
without producing invalid HTML/hydration issues.
- Around line 30-40: The submit flow allows duplicate requests because the
button only checks form.formState.isValid and not the mutation state; update the
prompt mutation from usePromptQuery to use the mutation's async API and pending
state (e.g., useMutation's mutateAsync and isPending) and change handleSubmit to
async/await calling mutateAsync, and include the mutation isPending flag in the
submit button's disabled prop (alongside !form.formState.isValid) so both the
board create handler (handleSubmit / createPrompt) and the other submit button
referenced later are disabled while the mutation is pending.

In `@components/ui/skeleton.tsx`:
- Around line 17-21: The Skeleton component uses a non-existent Tailwind class
"h-159.5"; update the className on the Skeleton element to use Tailwind
arbitrary value syntax (e.g., h-[159.5px] or h-[9.96875rem]) instead of h-159.5
so the height is applied correctly; keep the rest of the attributes (p-4,
bg-gray-100/60, data-slot="board-skeleton", aria-hidden) unchanged and ensure
the change is made on the Skeleton JSX element in components/ui/skeleton.tsx.

In `@lib/utils.ts`:
- Around line 20-21: The current stripHtml function uses a regex that removes
anything between angle brackets and thus also deletes legitimate plaintext like
"a < b" or code snippets; replace this regex-based implementation in stripHtml
with a parser-based approach: if running in a browser use DOMParser to parse the
html string and return document.body.textContent.trim(), otherwise (Node) use a
robust HTML parser (e.g., htmlparser2 or parse5) to build a DOM and extract only
text nodes (preserving literal characters inside text and code/pre blocks), and
fall back to using the editor's plain-text value when available instead of
stripping HTML strings.

In `@mocks/handlers/prompt-list-handlers.ts`:
- Around line 4-47: The mock array MOCK_PROMPTS uses an id field but the API
type (PromptResponse in queries/api/prompts.ts) expects promptId; update the
mock objects to replace id with promptId and adjust any handlers or references
in this file that read MOCK_PROMPTS to use promptId (or map fields to
PromptResponse shape) so the mock data matches the PromptResponse shape used by
functions that consume MOCK_PROMPTS.

In `@proxy.ts`:
- Line 12: session?.isNewUser becomes undefined in dev-mode fallback and breaks
the sign-in flow; update the logic that computes isNewUser (and any dev-mode
fallback for isSignIn) so it has a deterministic boolean in dev mode.
Concretely, in proxy.ts locate where session, isSignIn and isNewUser are derived
and ensure isNewUser is explicitly initialized (e.g., false or a dev-mode value)
when session is null/mocked so the downstream checks in the sign-in block (the
if (isSignIn) { ... } that references isNewUser, pathname, ROUTES.auth.SIGNUP
and isAuthRoute) behave correctly; alternatively add a paired dev-mode branch
that sets both isSignIn and isNewUser consistently or gate the redirect logic
behind a proper env/dev flag or mock session helper.
- Around line 10-11: The current logic uses NODE_ENV to bypass auth
(isDevelopment) causing security and consistency issues; change the bypass to an
explicit opt-in env var (e.g., NEXT_PUBLIC_BYPASS_AUTH === "true") and compute
isSignIn as !!session || (process.env.NEXT_PUBLIC_BYPASS_AUTH === "true"),
update any dependent logic (isNewUser calculation and the check if (isNewUser &&
pathname !== ROUTES.auth.SIGNUP)) so it derives from the same session/bypass
source, and prefer using MSW to provide a consistent mock session for
development/testing instead of flipping NODE_ENV-based bypass.

In `@queries/api/prompt.guide.md`:
- Around line 19-22: The PromptCreateRequest title length in the docs is
wrong—update the API guide to match the implementation by changing the title max
from 200 to 100 characters; align the doc entry for PromptCreateRequest with
board-schema.ts where the title uses .max(100) (and note consistency with
MAX_BOARD_CONTENT_LENGTH for content).

In `@queries/api/prompts.ts`:
- Around line 5-19: The Prompt interface's promptId type is inconsistent with
PromptResponse (Prompt.promptId is string while PromptResponse.promptId is
number); update the Prompt interface so promptId is the same type as
PromptResponse (change Prompt.promptId to number) and then update any usages of
Prompt.promptId (e.g., codepaths that parse/assign promptId) to handle it as a
number, or alternatively change PromptResponse.promptId to string if the backend
returns strings — pick the backend-canonical type and make Prompt and
PromptResponse match.
- Around line 35-40: PromptCreateRequest defines categoryId as string but
getPrompts.params.categoryId is typed as number, causing a type mismatch; decide
which type matches the backend and make both sides consistent: either change
PromptCreateRequest.categoryId to number (and update any form submission
conversion logic) or change getPrompts.params.categoryId to string (and adjust
callers), then update related types like PromptUpdateRequest and any places
referencing PromptCreateRequest, PromptUpdateRequest or getPrompts to the chosen
type to ensure uniformity.

---

Outside diff comments:
In `@mocks/handlers/prompt-list-handlers.ts`:
- Line 128: Remove the stray console.log call that prints `Prompt ${promptId}
좋아요 취소됨` from mocks/handlers/prompt-list-handlers.ts; locate the line
referencing promptId in the handler (the console.log within the
unlike/unfavorite handler) and delete it, or replace it with the project's
standardized logger (e.g., use existing processLogger or a logger.info/debug
call) if persistent logging is required.

---

Nitpick comments:
In `@app/`(afterLogin)/prompt/[id]/_components/comment-form.tsx:
- Around line 42-58: Remove the unnecessary try-catch inside the mutation's
onSuccess: directly call setComment("") and queryClient.invalidateQueries({
queryKey: promptQueries.comments(promptId).queryKey }) then call
toasts.success("댓글이 등록되었습니다.") and onSuccess?.(); also implement onError to
surface failures to the user (e.g., call toasts.error with a helpful message) so
users get feedback when the mutation fails; keep references to setComment,
queryClient.invalidateQueries, promptQueries.comments(promptId).queryKey,
toasts.success, onSuccess, and onError when making these changes.

In `@app/`(afterLogin)/prompt/[id]/_components/comment-item.tsx:
- Around line 79-106: The like and report buttons in CommentItem currently have
no onClick handlers; add explicit handlers (e.g., handleLikeToggle and
handleReportClick) to the like button and report button respectively or, if
deferred, add TODO comments above the JSX to indicate planned work. Implement
handleLikeToggle to call the like-toggle mutation/dispatch and update optimistic
UI state in CommentItem, and implement handleReportClick to open the report
modal or navigate to the report flow (invoke the modal open function or router).
Ensure the button elements (the ThumbsUp and Siren buttons) include the onClick
prop wired to these handlers and proper accessible labels/disabled handling
while the operations are in-flight.
- Around line 37-43: ProfIcon is being rendered with src={null} which hardcodes
the image source; update the ProfIcon usage in comment-item.tsx to pass the
actual avatar/icon field from the comment object (e.g., comment.avatar,
comment.userIcon or comment.user?.icon depending on the shape returned by
PromptCommentResponse) and keep fallback={comment.nickName} for missing images;
if PromptCommentResponse lacks an icon/avatar property, coordinate with backend
to add a clear field (name it avatar or icon) and then wire that property into
ProfIcon's src.

In `@app/`(afterLogin)/prompt/[id]/_components/comment-list.tsx:
- Around line 50-70: getRepliesForComment is recomputed and called repeatedly
inside sortedComments causing O(n*m) work; memoize it or pre-group replies into
a Map to avoid repeated filtering. Update the component to derive a
repliesByParent map via useMemo (dependent on replies) and change
getRepliesForComment to return repliesByParent.get(commentId) || [] (or wrap
getRepliesForComment with useCallback that reads the memoized map); then update
sortedComments to use that map-backed lookup instead of filtering the replies
array each time.

In `@app/`(afterLogin)/prompt/[id]/_components/post-detail.tsx:
- Around line 95-102: The handleCopy callback silently ignores failures; update
its catch block to show a failure toast so users get feedback when
navigator.clipboard.writeText(prompt.content) fails—use the existing toasts
utility (e.g., toasts.error("복사에 실패했습니다.") or a localized message) and
optionally log the error for debugging; ensure handleCopy still depends on
prompt.content.
- Around line 107-111: The AuthorProfileCard is being passed prompt.userDesc
without a fallback which can yield undefined rendering issues; update the JSX
that renders AuthorProfileCard to pass a safe fallback for the userDesc prop
(e.g., use prompt.userDesc ?? "" or another default string) alongside the
existing prompt.userIcon ?? "" and prompt.userName ?? "" so all three props
consistently have defaults.

In `@app/`(afterLogin)/prompt/board-schema.ts:
- Around line 13-19: 현재 content 필드의 유효성 검사 순서가 잘못되어 z.string().min(1) 검사가
stripHtml보다 먼저 실행돼 "<p></p>" 같은 빈 HTML이 통과될 수 있으므로, content 검증 순서를 변경해
stripHtml로 HTML 제거 후 길이 검사(refine)로 빈 문자열/최대 길이(MAX_BOARD_CONTENT_LENGTH)를 확인하도록
수정하고, 관련 로직에서 사용하는 함수명(stripHtml)과 상수(MAX_BOARD_CONTENT_LENGTH)를 그대로 유지해 위치를 찾아
적용하세요; 또한 파일에 남아있는 주석 처리된 category 관련 코드(주석된 라인)는 제거하세요.

In `@app/`(afterLogin)/prompt/hook/use-prompt-query.tsx:
- Around line 8-23: Rename the hook usePromptQuery to a name that reflects it
only performs creation (e.g., useCreatePrompt or usePromptCreateMutation) and
update the default export accordingly; inside the mutation returned (currently
using mutationFn: promptQueries.create) replace the onError body that only
console.errors with calling your toast error helper (e.g., toasts.error)
including a brief user-facing message and any safe error detail, and keep the
existing queryClient.invalidateQueries call in onSuccess and the toasts.success
call unchanged; ensure any callers/imports of usePromptQuery are updated to the
new hook name.

In `@components/board/board.tsx`:
- Around line 54-58: The character counter UI in board.tsx currently always
renders neutral styling; update the counter so when stripHtml(value).length >
MAX_BOARD_CONTENT_LENGTH the text uses an error style (e.g., change the
className to include a red text class) and otherwise keeps the existing gray
class. Locate the counter markup that renders {stripHtml(value).length} /
{MAX_BOARD_CONTENT_LENGTH} and conditionally apply the red styling (or a CSS
error class) based on the length check so the count turns red when the limit is
exceeded.
- Around line 29-42: Board currently accepts an untyped ...props which is unsafe
and can accidentally override handlers from react-hook-form; update BoardProps
to explicitly type/omit potential conflicts by deriving remaining props from the
underlying QuillBoard (or a DOM element) and excluding controlled keys: e.g.
change BoardProps to extend Omit<React.ComponentPropsWithoutRef<typeof
QuillBoard>,'value'|'onChange'|'placeholder'|'error'> (or
Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'ref'>) and rename
...props to something like htmlProps, then spread htmlProps when rendering
QuillBoard (ensure you do not spread before explicitly passing
value/setValue/error) so onChange/ref from react-hook-form cannot be
unintentionally overridden.

In `@mocks/handlers/prompt-list-handlers.ts`:
- Around line 259-267: The DELETE handler defined with http.delete that
currently destructures params to get promptId but never uses it should either
include the deleted ID in the response or stop extracting it; update the handler
(the http.delete callback and its params destructuring) so the JSON response
contains the promptId (e.g., include promptId in the message or a dedicated
field like deletedId) or remove the { promptId } destructuring if you
intentionally don't return it, ensuring the variable is not unused.
- Around line 114-137: Remove the duplicate like handlers implemented as
http.post(`${BASE_URL}/prompts/like/:promptId`, ...) and
http.delete(`${BASE_URL}/prompts/like/:promptId`, ...) and keep only the
canonical handler http.post(`${BASE_URL}/prompts/:promptId/like`, ...) as
specified in the API guide; delete the POST/DELETE blocks for
`/prompts/like/:promptId`, ensure any test/mocking references point to
`/prompts/:promptId/like`, and remove the redundant console.log in the deleted
handlers if it was the only side effect.

In `@package.json`:
- Line 22: You currently have validation logic split between guide.md, front-end
Zod schemas and backend DTOs which risks drift; consolidate the contract by
generating Zod schemas from the server DTOs or moving DTOs + Zod schemas into a
shared package/OpenAPI codegen so form validation and API DTOs share one source
of truth; update package.json (the "@hookform/resolvers" dependency and related
front-end schema files) to consume the shared contract (or the generated
schemas) and remove duplicated schema copies referenced in guide.md, and adjust
import paths in form code to import the canonical Zod schemas instead of local
copies.

In `@queries/options/prompt-query.ts`:
- Around line 81-91: The parameter types for createReply, updateComment, and
deleteComment are narrowed to string but promptApi methods accept string |
number; update the function signatures for createReply, updateComment, and
deleteComment to use promptId/commentId: string | number (matching createComment
and promptApi), or if the string-only restriction is intentional, add a clear
comment on each method (createReply, updateComment, deleteComment) explaining
why numbers are disallowed; ensure the types align with promptApi.createReply,
promptApi.updateComment, and promptApi.deleteComment to maintain type
consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2ca00ea7-2b7b-455a-b1f7-fd11a239f572

📥 Commits

Reviewing files that changed from the base of the PR and between a14dae2 and 0212e9e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (32)
  • .gitignore
  • .vscode/mcp.json
  • app/(afterLogin)/(dashboard)/(community-category)/community/[id]/layout.tsx
  • app/(afterLogin)/(dashboard)/(community-category)/layout.tsx
  • app/(afterLogin)/prompt/[id]/_components/comment-form.tsx
  • app/(afterLogin)/prompt/[id]/_components/comment-item.tsx
  • app/(afterLogin)/prompt/[id]/_components/comment-list.tsx
  • app/(afterLogin)/prompt/[id]/_components/post-detail.tsx
  • app/(afterLogin)/prompt/[id]/_components/utils.ts
  • app/(afterLogin)/prompt/[id]/edit/page.tsx
  • app/(afterLogin)/prompt/[id]/page.tsx
  • app/(afterLogin)/prompt/board-navigate.tsx
  • app/(afterLogin)/prompt/board-schema.ts
  • app/(afterLogin)/prompt/constant.ts
  • app/(afterLogin)/prompt/hook/use-prompt-query.tsx
  • app/(afterLogin)/prompt/layout.tsx
  • app/(afterLogin)/prompt/query-option.ts
  • app/(afterLogin)/prompt/write/page.tsx
  • app/(afterLogin)/prompt/write/write-board.tsx
  • app/styles/globals.css
  • components/board/board.tsx
  • components/search/search-empty.tsx
  • components/ui/skeleton.tsx
  • lib/routes.ts
  • lib/utils.ts
  • mocks/handlers/handlers.ts
  • mocks/handlers/prompt-list-handlers.ts
  • package.json
  • proxy.ts
  • queries/api/prompt.guide.md
  • queries/api/prompts.ts
  • queries/options/prompt-query.ts
💤 Files with no reviewable changes (1)
  • app/styles/globals.css

Comment thread .vscode/mcp.json
Comment on lines +3 to +5
"mcp-figma": {
"url": "http://127.0.0.1:3845/mcp",
"type": "http"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## MCP / VS Code 관련 문서 및 설정 참조 검색"
rg -n -C2 \
  -g '!**/node_modules/**' \
  -g '!**/.next/**' \
  -g '!**/build/**' \
  -g '!**/dist/**' \
  '(mcp-figma|127\.0\.0\.1:3845|\.vscode/mcp\.json|\bMCP\b|Model Context Protocol|Figma)' .

echo
echo "## 현재 추적 중인 .vscode 파일 목록"
git ls-files '.vscode/**'

Repository: Inflearn-Prog/Prog-fe

Length of output: 598


로컬 MCP 엔드포인트를 문서화하거나 버전 관리 대상에서 제외하세요.

.vscode/mcp.json이 저장소에 추적되고 있지만, 이 설정을 어떻게 시작해야 하는지에 대한 문서가 없습니다. 새로운 팀원이 저장소를 클론했을 때 127.0.0.1:3845에서 MCP 서버가 실행 중이지 않으면 에디터 연동이 즉시 실패합니다.

해결 방법 중 선택:

  1. 팀 공용 설정으로 유지하려면: README.md 또는 CONTRIBUTING.md에 MCP 설정 및 로컬 서버 실행 가이드 추가 (포트 번호, 실행 명령어 포함)
  2. 개인 환경 설정이라면: .gitignore.vscode/mcp.json 추가하고, .vscode/mcp.json.example 또는 docs/ 아래 템플릿 파일 제공

로컬호스트는 절대 문서화 없이는 공유할 수 없습니다. 명시적인 설정 가이드가 있으면 초기 셋업 장벽이 크게 낮아집니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vscode/mcp.json around lines 3 - 5, The tracked .vscode/mcp.json contains a
local MCP endpoint ("mcp-figma" with "url": "http://127.0.0.1:3845/mcp") and
needs either documentation or exclusion: either add a setup section to README.md
or CONTRIBUTING.md describing how to run the local MCP server (port 3845 and the
exact start command) so teammates can reproduce the endpoint, or treat the file
as per-developer config by adding .vscode/mcp.json to .gitignore and committing
a template like .vscode/mcp.json.example or docs/mcp.md that shows the
"mcp-figma" key and URL format for users to copy.

Comment thread app/(afterLogin)/prompt/[id]/edit/page.tsx Outdated
Comment on lines +20 to +22
<button onClick={handleGoBack}>
<ArrowLeftIcon className="size-9" />
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

아이콘 버튼에 접근 가능한 이름이 빠져 있습니다.

지금 버튼은 스크린리더에서 용도를 알 수 없는 “버튼”으로만 읽힙니다. 뒤로 가기는 핵심 내비게이션이라 aria-label="뒤로가기" 정도는 꼭 붙여두는 편이 좋아요. 덤으로 type="button"까지 명시해 두면 나중에 폼 안으로 들어가도 의도치 않은 submit을 막을 수 있습니다. 접근성 쪽은 작은 속성 하나로 꽤 큰 점수를 챙깁니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/board-navigate.tsx around lines 20 - 22, The button
element using onClick={handleGoBack} with <ArrowLeftIcon /> lacks accessible
name and explicit type; update that button to include aria-label="뒤로가기" (or an
appropriate localized label) and add type="button" to prevent accidental form
submits, ensuring the clickable icon is announced properly by screen readers and
safe inside forms.

Comment on lines +26 to +28
<BaseButton className="label-medium" variant="secondary">
임시저장된 글(1)
</BaseButton>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

동작 없는 임시저장 버튼을 그대로 노출하면 사용자만 헷갈립니다.

임시저장된 글(1)이 하드코딩되어 있고 실제 동작도 연결되어 있지 않아서, 클릭 가능한 컨트롤처럼 보이지만 아무 일도 일어나지 않습니다. 임시저장 기능/카운트가 아직 없으면 비활성화하거나 숨기고, 준비되면 실제 개수와 이동 동선을 연결해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/board-navigate.tsx around lines 26 - 28, The "임시저장된
글(1)" BaseButton in board-navigate.tsx is hardcoded and non-functional; change
it so that it either hides or renders disabled when there is no draft
feature/count, and when drafts exist wire its onClick to the real navigation
handler (e.g., call the existing router/navigation function used elsewhere in
this component) and replace the hardcoded "(1)" with a dynamic draftCount value
from props/state (or a selector) so the label and click behavior reflect actual
data; use BaseButton's disabled prop when showing but not actionable, or
conditionally render nothing when the feature is unavailable.

Comment thread app/(afterLogin)/prompt/layout.tsx Outdated
Comment on lines +14 to +16
<div className="w-full bg-white h-15 flex items-center mb-12.5">
<BoardNavigate />
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

BoardNavigate가 상세 페이지에서도 "게시글 작성하기"를 표시합니다.

app/(afterLogin)/prompt/board-navigate.tsx를 보면 BoardNavigate가 항상 "게시글 작성하기" 제목과 "임시저장된 글" 버튼을 렌더링합니다. 이 레이아웃은 작성 페이지(/prompt/write)와 상세 페이지(/prompt/[id]) 모두에 적용되므로, 상세 페이지에서 부적절한 UI가 노출됩니다.

개선 방안:

  1. BoardNavigatemode prop을 추가하여 "write" / "detail" 분기 처리
  2. 또는 작성 페이지와 상세 페이지에 별도의 레이아웃 적용
💡 BoardNavigate 개선 예시
// board-navigate.tsx
interface BoardNavigateProps {
  mode?: "write" | "detail";
  title?: string;
}

export default function BoardNavigate({ mode = "write", title }: BoardNavigateProps) {
  return (
    <div className={layout}>
      <div className="flex items-center gap-x-5">
        <button onClick={handleGoBack}>
          <ArrowLeftIcon className="size-9" />
        </button>
        <h2 className="heading-medium">
          {mode === "write" ? "게시글 작성하기" : (title ?? "게시글 상세")}
        </h2>
      </div>
      {mode === "write" && (
        <BaseButton className="label-medium" variant="secondary">
          임시저장된 글(1)
        </BaseButton>
      )}
    </div>
  );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/layout.tsx around lines 14 - 16, BoardNavigate in
board-navigate.tsx is always rendering "게시글 작성하기" and the temp-save button which
is inappropriate on the detail page; update BoardNavigate to accept a mode prop
(e.g., mode?: "write" | "detail" and optional title?) and branch rendering so
that when mode === "write" it shows the "게시글 작성하기" title and the 임시저장된 글 button,
and when mode === "detail" it shows a detail title (use passed title or
fallback) and hides the temp-save button; then update where BoardNavigate is
used in layout.tsx (or pass mode from prompt/write and prompt/[id] pages) so the
write page passes mode="write" and the detail page passes mode="detail".

Comment thread proxy.ts
Comment on lines +10 to +11
const isDevelopment = process.env.NODE_ENV === "development";
const isSignIn = isDevelopment ? true : !!session;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: auth() 직접 호출하는 서버 컴포넌트 찾기
# Expect: 미들웨어 우회와 무관하게 동작하는 컴포넌트들 확인

rg -n "from ['\"]@/lib/auth['\"]" --type=ts --type=tsx -A 3 | head -50

Repository: Inflearn-Prog/Prog-fe

Length of output: 92


🏁 Script executed:

# Find auth imports and usages
rg "from.*@/lib/auth" --type=ts -A 3 | head -30

Repository: Inflearn-Prog/Prog-fe

Length of output: 1038


🏁 Script executed:

# Search for direct auth() calls in the codebase
rg "auth\(\)" --type=ts -B 2 -A 2 | head -50

Repository: Inflearn-Prog/Prog-fe

Length of output: 841


🏁 Script executed:

# Read the proxy.ts file to understand the full context
cat proxy.ts

Repository: Inflearn-Prog/Prog-fe

Length of output: 1630


개발 모드 인증 우회는 보안 위험과 심각한 일관성 문제를 야기합니다.

코드베이스 검토 결과, 이 구현에는 실제로 세 가지 문제가 있습니다:

  1. 보안 위험 - NODE_ENV 설정 실수: NODE_ENV="development"이 설정되면 모든 인증이 무조건 우회됩니다. 배포 환경에서 NODE_ENV가 잘못 설정된 경우 실제 세션 없이도 로그인된 것처럼 동작하게 됩니다.

  2. 서버 컴포넌트와의 심각한 불일치: 프로젝트 여러 곳의 서버 컴포넌트(app/(afterLogin)/prompt/[id]/page.tsx, app/(afterLogin)/(dashboard)/(community-category)/layout.tsx 등)에서 auth()를 직접 호출합니다. 미들웨어는 개발 모드에서 isSignIn=true로 처리하지만, 이 서버 컴포넌트들은 실제 session 값(null 포함)을 받습니다. 결과적으로:

    • 미들웨어: 로그인됨 → 신규 사용자 리다이렉트 로직 실행
    • 서버 컴포넌트: session=null → 실제 로그인하지 않은 상태로 처리

    동일한 사용자가 미들웨어와 서버 컴포넌트에서 다르게 인식됩니다.

  3. 신규 사용자 로직 버그: 개발 모드에서 session이 null일 때, isNewUser = undefined가 되고, 30번 줄의 if (isNewUser && pathname !== ROUTES.auth.SIGNUP) 조건이 거짓이 되어 신규 사용자 가입 페이지 리다이렉트가 실행되지 않습니다.

권장 대안:

🛡️ Option 1: 별도 환경 변수 사용 (보안 우선)
- const isDevelopment = process.env.NODE_ENV === "development";
- const isSignIn = isDevelopment ? true : !!session;
+ const isSignIn = process.env.NEXT_PUBLIC_BYPASS_AUTH === "true" || !!session;

.env.local에서만 명시적으로 NEXT_PUBLIC_BYPASS_AUTH=true를 설정하고, .env.production에는 포함하지 않으면 프로덕션 배포 시 우회가 발생하지 않습니다. NODE_ENV 설정 실수로부터 안전합니다.

💡 Option 2: MSW 활용한 일관된 세션 제공 (권장)

프로젝트에서 이미 MSW를 사용 중이므로, mock 핸들러에서 테스트용 세션 객체를 반환하도록 설정하면:

  • 미들웨어와 서버 컴포넌트가 동일한 세션 객체를 받음
  • 인증 관련 로직을 통합적으로 테스트 가능
  • 프로덕션 코드 수정 없음
  • 개발/테스트 환경의 일관성 확보
📝 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
const isDevelopment = process.env.NODE_ENV === "development";
const isSignIn = isDevelopment ? true : !!session;
const isSignIn = process.env.NEXT_PUBLIC_BYPASS_AUTH === "true" || !!session;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy.ts` around lines 10 - 11, The current logic uses NODE_ENV to bypass
auth (isDevelopment) causing security and consistency issues; change the bypass
to an explicit opt-in env var (e.g., NEXT_PUBLIC_BYPASS_AUTH === "true") and
compute isSignIn as !!session || (process.env.NEXT_PUBLIC_BYPASS_AUTH ===
"true"), update any dependent logic (isNewUser calculation and the check if
(isNewUser && pathname !== ROUTES.auth.SIGNUP)) so it derives from the same
session/bypass source, and prefer using MSW to provide a consistent mock session
for development/testing instead of flipping NODE_ENV-based bypass.

Comment thread proxy.ts
const isSignIn = !!session;
const isDevelopment = process.env.NODE_ENV === "development";
const isSignIn = isDevelopment ? true : !!session;
const isNewUser = session?.isNewUser;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

개발 모드에서 isNewUser가 항상 undefined가 되어 로직 오류가 발생합니다.

개발 모드에서 sessionnull일 때:

  • isSignIn = true (우회로 인해)
  • isNewUser = session?.isNewUser = undefined

이 상태에서 Line 30-40의 로직이 실행되면:

if (isSignIn) {  // true (개발 모드)
  if (isNewUser && pathname !== ROUTES.auth.SIGNUP) {  // undefined는 falsy → 스킵
    // 신규 유저 리다이렉트 로직이 동작하지 않음
  }
  if (!isNewUser && isAuthRoute) {  // !undefined = true → 실행됨
    // 로그인 페이지 접근 시 무조건 메인으로 리다이렉트
  }
}

결과적으로 개발 모드에서 /signin 페이지에 접근하면 무조건 메인으로 리다이렉트되어, 실제 로그인 테스트가 어려워집니다.

🔧 수정 제안

만약 개발 모드 우회를 유지하려면, isNewUser도 함께 처리해야 합니다:

  const isDevelopment = process.env.NODE_ENV === "development";
  const isSignIn = isDevelopment ? true : !!session;
- const isNewUser = session?.isNewUser;
+ const isNewUser = isDevelopment ? false : session?.isNewUser;

하지만 앞서 언급한 대로, 근본적으로는 별도 환경 변수나 mock 세션 사용을 권장드립니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy.ts` at line 12, session?.isNewUser becomes undefined in dev-mode
fallback and breaks the sign-in flow; update the logic that computes isNewUser
(and any dev-mode fallback for isSignIn) so it has a deterministic boolean in
dev mode. Concretely, in proxy.ts locate where session, isSignIn and isNewUser
are derived and ensure isNewUser is explicitly initialized (e.g., false or a
dev-mode value) when session is null/mocked so the downstream checks in the
sign-in block (the if (isSignIn) { ... } that references isNewUser, pathname,
ROUTES.auth.SIGNUP and isAuthRoute) behave correctly; alternatively add a paired
dev-mode branch that sets both isSignIn and isNewUser consistently or gate the
redirect logic behind a proper env/dev flag or mock session helper.

Comment on lines +19 to +22
- **PromptCreateRequest**
- `categoryId` (Long, 필수): 카테고리 ID
- `title` (String, 필수): 제목 (최대 200자)
- `content` (String, 필수): 내용 (최대 5000자)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# MAX_BOARD_CONTENT_LENGTH 상수 값 확인
rg -n "MAX_BOARD_CONTENT_LENGTH" --type ts -A1

Repository: Inflearn-Prog/Prog-fe

Length of output: 893


🏁 Script executed:

cat -n app/(afterLogin)/prompt/board-schema.ts

Repository: Inflearn-Prog/Prog-fe

Length of output: 198


🏁 Script executed:

find . -name "board-schema.ts" -type f -exec cat -n {} \;

Repository: Inflearn-Prog/Prog-fe

Length of output: 798


문서에서 제목 최대 길이를 100자로 수정하세요.

실제 구현과 API 문서가 일치하지 않습니다:

  • 문서: title (최대 200자)
  • 구현: board-schema.ts line 11에서 .max(100)

반면 content는 올바릅니다:

  • 문서: content (최대 5000자)
  • 구현: MAX_BOARD_CONTENT_LENGTH = 5000

프론트엔드 유효성 검사와 API 명세가 동기화되어 있지 않으면 사용자가 혼란을 겪을 수 있습니다. API 문서의 19번 라인을 다음과 같이 수정해주세요:

- `title` (String, 필수): 제목 (최대 100자)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@queries/api/prompt.guide.md` around lines 19 - 22, The PromptCreateRequest
title length in the docs is wrong—update the API guide to match the
implementation by changing the title max from 200 to 100 characters; align the
doc entry for PromptCreateRequest with board-schema.ts where the title uses
.max(100) (and note consistency with MAX_BOARD_CONTENT_LENGTH for content).

Comment thread queries/api/prompts.ts
Comment on lines +5 to +19
export interface Prompt {
id: number;
userId: number;
category: JobCategory;
title: string;
content: string;
createdAt: string;
updatedAt: string;
userName: string | null;
userIcon: string | null;
userDesc: string;
likes: number;
isLiked: boolean;
promptId: string;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

타입 일관성 문제: promptId 타입 불일치

Prompt 인터페이스의 promptIdstring(Line 18)인 반면, PromptResponsepromptIdnumber(Line 54)입니다. 이로 인해 런타임에서 타입 불일치 문제가 발생할 수 있습니다.

백엔드 API 응답 타입을 확인하고 일관된 타입으로 통일하세요.

🔧 타입 통일 제안
 export interface Prompt {
   id: number;
   userId: number;
   category: JobCategory;
   title: string;
   content: string;
   createdAt: string;
   updatedAt: string;
   userName: string | null;
   userIcon: string | null;
   userDesc: string;
   likes: number;
   isLiked: boolean;
-  promptId: string;
+  promptId: number;
 }
📝 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
export interface Prompt {
id: number;
userId: number;
category: JobCategory;
title: string;
content: string;
createdAt: string;
updatedAt: string;
userName: string | null;
userIcon: string | null;
userDesc: string;
likes: number;
isLiked: boolean;
promptId: string;
}
export interface Prompt {
id: number;
userId: number;
category: JobCategory;
title: string;
content: string;
createdAt: string;
updatedAt: string;
userName: string | null;
userIcon: string | null;
userDesc: string;
likes: number;
isLiked: boolean;
promptId: number;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@queries/api/prompts.ts` around lines 5 - 19, The Prompt interface's promptId
type is inconsistent with PromptResponse (Prompt.promptId is string while
PromptResponse.promptId is number); update the Prompt interface so promptId is
the same type as PromptResponse (change Prompt.promptId to number) and then
update any usages of Prompt.promptId (e.g., codepaths that parse/assign
promptId) to handle it as a number, or alternatively change
PromptResponse.promptId to string if the backend returns strings — pick the
backend-canonical type and make Prompt and PromptResponse match.

Comment thread queries/api/prompts.ts
Comment on lines +35 to +40
// 1. 프롬프트 게시글 관련 타입
export interface PromptCreateRequest extends Pick<Prompt, "title" | "content"> {
categoryId: string;
}

export type PromptUpdateRequest = Partial<PromptCreateRequest>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: categoryId 타입 사용 패턴 확인

# 프로젝트 내 categoryId 사용 검색
rg -n 'categoryId' --type=ts -C2

Repository: Inflearn-Prog/Prog-fe

Length of output: 1425


🏁 Script executed:

# Prompt 인터페이스 정의 찾기
rg -n 'interface Prompt' --type=ts -A10

Repository: Inflearn-Prog/Prog-fe

Length of output: 4746


🏁 Script executed:

# write-board.tsx에서 폼 데이터 타입 확인
rg -n 'data\.category' --type=ts -B5 -A2

Repository: Inflearn-Prog/Prog-fe

Length of output: 694


🏁 Script executed:

# 전체 categoryId 타입 정의 패턴 확인
rg -n 'categoryId\s*[:=]' --type=ts -B2 -A1

Repository: Inflearn-Prog/Prog-fe

Length of output: 620


🏁 Script executed:

# BoardFormData 타입 정의 찾기
rg -n 'interface BoardFormData|type BoardFormData' --type=ts -A8

Repository: Inflearn-Prog/Prog-fe

Length of output: 164


🏁 Script executed:

# JobCategory 타입 정의 찾기
rg -n 'JobCategory' --type=ts -B2 -A5

Repository: Inflearn-Prog/Prog-fe

Length of output: 660


🏁 Script executed:

# write-board.tsx의 form 설정 확인
rg -n 'useForm|category' app/\(afterLogin\)/prompt/write/write-board.tsx --type=ts -B3 -A3

Repository: Inflearn-Prog/Prog-fe

Length of output: 1331


🏁 Script executed:

# boardSchema 정의 확인
rg -n 'boardSchema' app/\(afterLogin\)/prompt/board-schema.ts --type=ts -B2 -A20

Repository: Inflearn-Prog/Prog-fe

Length of output: 625


타입 불일치: categoryId 타입 정의 일관성

PromptCreateRequest.categoryIdstring 타입이지만, getPromptsparams.categoryIdnumber 타입으로 정의되어 있습니다(Line 87). 현재 폼에서는 문자열("BACKEND", "FRONTEND" 등)을 전송하는데, 조회 API는 숫자를 기대하고 있어 일관성이 깨져 있습니다.

개선 방안:

  1. 카테고리 ID가 실제로 문자열이라면, getPrompts 파라미터도 string으로 맞추세요.
  2. 카테고리 ID가 실제로 숫자라면, PromptCreateRequest.categoryIdnumber로 변경하고 폼 데이터 변환 로직을 추가하세요.

백엔드 API 스펙을 확인한 후 한쪽으로 통일해주세요. 타입 일관성은 런타임 에러를 방지하는 첫 번째 방어선입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@queries/api/prompts.ts` around lines 35 - 40, PromptCreateRequest defines
categoryId as string but getPrompts.params.categoryId is typed as number,
causing a type mismatch; decide which type matches the backend and make both
sides consistent: either change PromptCreateRequest.categoryId to number (and
update any form submission conversion logic) or change
getPrompts.params.categoryId to string (and adjust callers), then update related
types like PromptUpdateRequest and any places referencing PromptCreateRequest,
PromptUpdateRequest or getPrompts to the chosen type to ensure uniformity.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
app/(afterLogin)/prompt/write/page.tsx (1)

1-5: 페이지 메타데이터 추가 권장

Next.js App Router에서는 metadata export로 페이지별 SEO를 설정할 수 있습니다. 게시글 작성 페이지에 적절한 타이틀을 추가하면 좋습니다.

♻️ 메타데이터 추가 제안
+import { Metadata } from "next";
+
 import { PromptForm } from "../_components/prompt-form";
 
+export const metadata: Metadata = {
+  title: "프롬프트 작성",
+  description: "새로운 프롬프트를 작성합니다.",
+};
+
 export default function PromptWrite() {
   return <PromptForm />;
 }

참고: Next.js Metadata API

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/write/page.tsx around lines 1 - 5, 현재 PromptWrite
페이지는 페이지 메타데이터가 없으므로 SEO 및 탭 타이틀을 위해 export const metadata를 추가하세요; 구체적으로 파일의 최상단
또는 PromptWrite 컴포넌트 바로 위에 export const metadata = { title: "게시글 작성" } (원하면
description 등 추가) 형태로 내보내고 기존 PromptForm 사용은 그대로 유지하여 PromptWrite와 함께 메타데이터가
적용되도록 합니다.
components/shared/toast.tsx (1)

24-36: successerror 메서드 간 옵션 사용 방식이 일관되지 않습니다.

success 메서드는 하드코딩된 값(position: "bottom-center", duration: 5 * 1000)을 사용하는 반면, error 메서드는 defaultToastOptions를 참조합니다. 향후 기본값 변경 시 두 메서드가 다르게 동작할 수 있습니다.

♻️ 일관성 개선 제안
 export const toasts = {
-  success: (message: string) =>
+  success: (message: string, options?: ToastOptions) =>
     toast(() => <ToastSuccess>{message}</ToastSuccess>, {
-      position: "bottom-center",
-      duration: 5 * 1000,
+      position: options?.position ?? defaultToastOptions.position,
+      duration: options?.duration ?? defaultToastOptions.duration,
       className: "bg-gray-950! w-full py-4 px-5 inline-flex justify-center",
     }),
   error: (message: string, options?: ToastOptions) =>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/shared/toast.tsx` around lines 24 - 36, The toasts.success
implementation hardcodes position and duration while toasts.error uses
defaultToastOptions, causing inconsistent defaults; update toasts.success (and
optionally add an options param) to derive position and duration from
defaultToastOptions (or from an options argument) like toasts.error does, and
keep the same className/ToastSuccess usage and toast invocation pattern
(symbols: toasts.success, toasts.error, defaultToastOptions, ToastSuccess,
toast) so both methods rely on the same default source.
app/(afterLogin)/prompt/_components/prompt-form.tsx (1)

42-50: useEffect 의존성 배열 개선 권장

form 객체를 의존성 배열에 포함하면 ESLint exhaustive-deps 경고를 피할 수 있지만, useForm이 반환하는 form 객체는 안정적이므로 실제로는 불필요합니다. form.reset만 사용하면 더 명확합니다.

♻️ 개선 제안
+  const { reset } = form;
+
   useEffect(() => {
     if (initialData) {
-      form.reset({
+      reset({
         title: initialData.title,
         category: initialData.category,
         content: initialData.content,
       });
     }
-  }, [initialData, form]);
+  }, [initialData, reset]);

참고: React Hook Form - reset API

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/_components/prompt-form.tsx around lines 42 - 50,
The effect currently lists the whole form object in the dependency array;
replace that with the stable reset function to be explicit and avoid unnecessary
re-renders—update the useEffect dependency array to depend on initialData and
form.reset (i.e., useEffect(..., [initialData, form.reset])) and keep the body
calling form.reset({ title: initialData.title, category: initialData.category,
content: initialData.content }); so you reference the stable reset method
returned by useForm instead of the entire form object.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(afterLogin)/prompt/_components/prompt-form.tsx:
- Line 31: Remove the leftover debugging console.log in the prompt form
component: delete the line console.log("initialData", initialData); from the
PromptForm/component in prompt-form.tsx (and any other stray console.* calls in
that module) so no debug logging remains before merge.
- Around line 55-70: The form currently passes boardSchema's category string
directly to the API but the backend expects a numeric Long; update handleSubmit
to map the category string to a numeric ID before building PromptCreateRequest /
PromptUpdateRequest by adding a categoryMap (e.g., Record<string, number>) and
using categoryMap[data.category] for categoryId in both the create and update
branches; also update the PromptCreateRequest and PromptUpdateRequest types to
declare categoryId as number so the payload types match the backend.

In `@queries/api/prompts.ts`:
- Around line 129-158: The comment endpoints currently include a duplicated API
version prefix causing requests to hit api/v1/api/v1/*; update the paths used in
createComment, createReply, updateComment, deleteComment, and getComments to
remove the redundant "api/v1/" prefix (use "comment/..." instead of
"api/v1/comment/...") so they align with the base URL and the prompts endpoints.

---

Nitpick comments:
In `@app/`(afterLogin)/prompt/_components/prompt-form.tsx:
- Around line 42-50: The effect currently lists the whole form object in the
dependency array; replace that with the stable reset function to be explicit and
avoid unnecessary re-renders—update the useEffect dependency array to depend on
initialData and form.reset (i.e., useEffect(..., [initialData, form.reset])) and
keep the body calling form.reset({ title: initialData.title, category:
initialData.category, content: initialData.content }); so you reference the
stable reset method returned by useForm instead of the entire form object.

In `@app/`(afterLogin)/prompt/write/page.tsx:
- Around line 1-5: 현재 PromptWrite 페이지는 페이지 메타데이터가 없으므로 SEO 및 탭 타이틀을 위해 export
const metadata를 추가하세요; 구체적으로 파일의 최상단 또는 PromptWrite 컴포넌트 바로 위에 export const
metadata = { title: "게시글 작성" } (원하면 description 등 추가) 형태로 내보내고 기존 PromptForm 사용은
그대로 유지하여 PromptWrite와 함께 메타데이터가 적용되도록 합니다.

In `@components/shared/toast.tsx`:
- Around line 24-36: The toasts.success implementation hardcodes position and
duration while toasts.error uses defaultToastOptions, causing inconsistent
defaults; update toasts.success (and optionally add an options param) to derive
position and duration from defaultToastOptions (or from an options argument)
like toasts.error does, and keep the same className/ToastSuccess usage and toast
invocation pattern (symbols: toasts.success, toasts.error, defaultToastOptions,
ToastSuccess, toast) so both methods rely on the same default source.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5ed11ca6-2581-43d3-a024-94575078f867

📥 Commits

Reviewing files that changed from the base of the PR and between 0212e9e and 6d3f848.

📒 Files selected for processing (6)
  • app/(afterLogin)/prompt/[id]/edit/page.tsx
  • app/(afterLogin)/prompt/_components/prompt-form.tsx
  • app/(afterLogin)/prompt/hook/use-prompt-query.tsx
  • app/(afterLogin)/prompt/write/page.tsx
  • components/shared/toast.tsx
  • queries/api/prompts.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/(afterLogin)/prompt/hook/use-prompt-query.tsx
  • app/(afterLogin)/prompt/[id]/edit/page.tsx

Comment thread app/(afterLogin)/prompt/_components/prompt-form.tsx Outdated
Comment on lines +55 to +70
const handleSubmit = (data: BoardFormData) => {
if (isEdit && initialData) {
const body: PromptUpdateRequest = {
title: data.title,
categoryId: data.category,
content: data.content,
};
updatePrompt({ id: initialData.id, data: body });
} else {
const body: PromptCreateRequest = {
title: data.title,
categoryId: data.category,
content: data.content,
};
createPrompt(body);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

How to map string enum values to numeric IDs in TypeScript forms?

💡 Result:

Use an explicit lookup table (Record) and convert at the form boundary (e.g., in onSubmit or with a field “value transformer”). With string enums there’s no automatic reverse mapping, so you should not expect TypeScript to give you value→key/value→id for free. [1]

1) Recommended “enum-like” strings + ID map (JS-friendly)

export const Status = {
  Draft: "draft",
  Published: "published",
} as const;

export type Status = typeof Status[keyof typeof Status];

export const StatusId = {
  [Status.Draft]: 1,
  [Status.Published]: 2,
} satisfies Record<Status, number>; // type-checked map

Then in your form submit:

type FormValues = { status: Status };

function toApiPayload(v: FormValues) {
  return { statusId: StatusId[v.status] };
}

(Record is designed for “map from a key union to a value type”.) [2]

2) If you already have a TypeScript enum

export enum StatusEnum {
  Draft = "draft",
  Published = "published",
}

export const StatusId: Record<StatusEnum, number> = {
  [StatusEnum.Draft]: 1,
  [StatusEnum.Published]: 2,
};

Convert on submit the same way:

type FormValues = { status: StatusEnum };
const statusId = StatusId[values.status];

3) Building <select> options from the map

const options = (Object.entries(StatusId) as Array<[Status, number]>).map(
  ([value, id]) => ({ value, label: value, id })
);

Use value in the control; use id when building the payload.

Sources: TypeScript handbook on enums (incl. string-enum behavior / reverse mapping) [1]; Record as a typed key→value mapping [2].

[1] (typescriptlang.org)
[2] (exploringjs.com)

Citations:


🏁 Script executed:

# Find the prompts.ts file or API type definitions
fd -type f -name "*.ts" -o -name "*.tsx" | grep -E "(prompt|api)" | head -20

Repository: Inflearn-Prog/Prog-fe

Length of output: 235


🏁 Script executed:

# Search for PromptCreateRequest and PromptUpdateRequest definitions
rg "PromptCreateRequest|PromptUpdateRequest" -A 5

Repository: Inflearn-Prog/Prog-fe

Length of output: 6045


🏁 Script executed:

# Search for category-related type definitions
rg "category|Category" --type ts --type tsx -B 2 -A 2 | head -50

Repository: Inflearn-Prog/Prog-fe

Length of output: 92


🏁 Script executed:

# Find boardSchema definition
fd -type f \( -name "*schema*" -o -name "*board*" \)

Repository: Inflearn-Prog/Prog-fe

Length of output: 235


🏁 Script executed:

# Search for BoardFormData definition
rg "BoardFormData" -B 2 -A 10

Repository: Inflearn-Prog/Prog-fe

Length of output: 3412


🏁 Script executed:

# Check board-schema.ts directly
find . -name "board-schema.ts" -o -name "board-schema.tsx"

Repository: Inflearn-Prog/Prog-fe

Length of output: 106


🏁 Script executed:

cat app/\(afterLogin\)/prompt/board-schema.ts

Repository: Inflearn-Prog/Prog-fe

Length of output: 644


카테고리 ID 타입 불일치 수정 필요

boardSchema에서 category 필드는 문자열(z.string())로 정의되어 있고, 현재 코드는 이 문자열 값을 그대로 categoryId로 전달하고 있습니다. 하지만 백엔드 API 명세(prompt.guide.md)에는 categoryIdLong 타입(숫자)으로 정의되어 있습니다.

개선 방법:

  1. 카테고리 문자열을 숫자 ID로 변환하는 매핑 테이블을 만들어주세요:

    const categoryMap: Record<string, number> = {
      "BACKEND": 1,
      "FRONTEND": 2,
      // ...
    };
  2. handleSubmit에서 변환 로직을 추가하세요:

    const body: PromptCreateRequest = {
      title: data.title,
      categoryId: categoryMap[data.category], // 문자열 → 숫자로 변환
      content: data.content,
    };
  3. PromptCreateRequest 타입도 categoryId: number로 수정해야 합니다.

API 명세와 실제 구현을 맞춰주지 않으면 런타임에 요청이 실패할 가능성이 높습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/_components/prompt-form.tsx around lines 55 - 70,
The form currently passes boardSchema's category string directly to the API but
the backend expects a numeric Long; update handleSubmit to map the category
string to a numeric ID before building PromptCreateRequest / PromptUpdateRequest
by adding a categoryMap (e.g., Record<string, number>) and using
categoryMap[data.category] for categoryId in both the create and update
branches; also update the PromptCreateRequest and PromptUpdateRequest types to
declare categoryId as number so the payload types match the backend.

Comment thread queries/api/prompts.ts
Comment on lines +129 to +158
createComment: (promptId: string | number, data: PromptCommentRequest) =>
fetcher
.post(`api/v1/comment/${promptId}`, { json: data })
.json<ApiResponse<PromptCommentResponse>>(),

// 대댓글 작성
createReply: (
promptId: string | number,
commentId: string | number,
data: PromptCommentRequest
) =>
fetcher
.post(`api/v1/comment/${promptId}/${commentId}`, { json: data })
.json<ApiResponse<PromptCommentResponse>>(),

// 댓글 수정
updateComment: (commentId: string | number, data: PromptCommentRequest) =>
fetcher
.patch(`api/v1/comment/${commentId}`, { json: data })
.json<ApiResponse<PromptCommentResponse>>(),

// 댓글 삭제
deleteComment: (commentId: string | number) =>
fetcher.delete(`api/v1/comment/${commentId}`).json<ApiResponse<void>>(),

// 댓글 목록 조회
getComments: (promptId: string | number) =>
fetcher
.get(`api/v1/comment/${promptId}`)
.json<ApiResponse<PromptCommentResponse[]>>(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: 프로젝트 내 API 경로 패턴 확인

# API 경로 사용 패턴 검색
rg -n 'fetcher\.(get|post|put|patch|delete)\(' --type=ts -A1 | head -50

Repository: Inflearn-Prog/Prog-fe

Length of output: 1383


🏁 Script executed:

# prompts.ts 파일의 전체 내용 확인 - API 경로 패턴 분석
cat -n queries/api/prompts.ts | sed -n '75,160p'

Repository: Inflearn-Prog/Prog-fe

Length of output: 3192


🏁 Script executed:

# 코드베이스 전체에서 'api/v1' 패턴 검색
rg 'api/v1' --type=ts -n

Repository: Inflearn-Prog/Prog-fe

Length of output: 1145


🏁 Script executed:

# 댓글 관련 모든 엔드포인트 확인
rg 'comment|reply' --type=ts -B2 -A2

Repository: Inflearn-Prog/Prog-fe

Length of output: 20393


댓글 API 경로에서 API 버전 prefix 중복 발생

프롬프트 엔드포인트는 prompts/를 사용하고, 댓글 엔드포인트는 api/v1/comment/를 사용하고 있습니다. 백엔드 base URL이 이미 http://localhost:8000/api/v1로 설정되어 있으므로, 댓글 요청은 http://localhost:8000/api/v1/api/v1/comment/... 형태로 중복 prefix가 발생합니다.

예상 동작
  • Prompt: api/v1 + prompts/${id} = ✓ api/v1/prompts/${id}
  • Comment: api/v1 + api/v1/comment/${id} = ✗ api/v1/api/v1/comment/${id} (중복)

수정 방안: 댓글 엔드포인트의 경로를 comment/로 변경하여 프롬프트 엔드포인트와 일관되게 만드세요. 예: fetcher.post(\comment/${promptId}`, { json: data })`

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@queries/api/prompts.ts` around lines 129 - 158, The comment endpoints
currently include a duplicated API version prefix causing requests to hit
api/v1/api/v1/*; update the paths used in createComment, createReply,
updateComment, deleteComment, and getComments to remove the redundant "api/v1/"
prefix (use "comment/..." instead of "api/v1/comment/...") so they align with
the base URL and the prompts endpoints.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/(afterLogin)/prompt/_components/prompt-form.tsx (1)

100-107: ⚠️ Potential issue | 🟡 Minor

CLSBox의 마크업 구조 수정이 필요합니다.

호출부에서 에러 메시지를 <p>로 전달하는데(Lines 103, 132), CLSBox 내부에서 다시 <p>로 감싸고 있어 <p><p>...</p></p> 구조가 됩니다. HTML 명세상 <p>는 블록 요소를 포함할 수 없어 브라우저가 자동으로 태그를 닫게 되고, 이로 인해 레이아웃이 깨지거나 hydration 경고가 발생할 수 있습니다.

해결 방법 (택1):

  1. CLSBox 내부 래퍼를 <div>로 변경
  2. 호출부에서 <p> 대신 <span> 사용
🔧 수정 제안 (CLSBox 수정)
 function CLSBox({
   children,
   text,
 }: {
   children: React.ReactNode;
   text: React.ReactNode;
 }) {
   return (
     <div className="relative pb-7.5">
       {children}
-      <p className="absolute bottom-1 left-0">{text}</p>
+      <div className="absolute bottom-1 left-0">{text}</div>
     </div>
   );
 }

Also applies to: 129-136, 181-193

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/_components/prompt-form.tsx around lines 100 - 107,
The CLSBox component is wrapping its children with a <p> which causes invalid
nested <p><p>...</p></p> markup when callers (e.g., where
form.formState.errors.category is rendered) already pass a <p>; update CLSBox to
use a non-paragraph wrapper (e.g., <div>) around its children to avoid nested
paragraph tags; locate CLSBox usage in prompt-form.tsx (the calls around the
category and error renderers) and ensure the changed wrapper fixes all
occurrences (also at the other similar call sites noted) so no callers need to
change.
🧹 Nitpick comments (6)
app/(afterLogin)/prompt/_components/prompt-form.tsx (2)

29-29: mx-auto 클래스가 중복 선언되어 있습니다.

skeleton 컴포넌트와 동일하게 mx-auto가 두 번 사용되고 있습니다.

🔧 수정 제안
-  const layout = cn("mx-auto max-w-7xl lg:px-0 px-5 min-w-90 mx-auto");
+  const layout = cn("mx-auto max-w-7xl lg:px-0 px-5 min-w-90");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/_components/prompt-form.tsx at line 29, The class
string assigned to the variable layout contains a duplicated "mx-auto" ("const
layout = cn(\"mx-auto max-w-7xl lg:px-0 px-5 min-w-90 mx-auto\");"); remove the
redundant "mx-auto" so the cn(...) call only includes it once (match the
skeleton component's spacing classes) to avoid duplicate class declarations.

41-49: useEffect 의존성 배열에 form 객체 포함 시 주의가 필요합니다.

form.reset() 호출 후 form 객체의 참조가 변경될 수 있어 무한 루프가 발생할 가능성이 있습니다. react-hook-form에서는 reset 함수만 의존성에 포함하는 것이 안전합니다.

🔧 수정 제안
  useEffect(() => {
    if (initialData) {
      form.reset({
        title: initialData.title,
        category: initialData.category,
        content: initialData.content,
      });
    }
-  }, [initialData, form]);
+  }, [initialData, form.reset]);

📚 참고: react-hook-form reset 문서

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/_components/prompt-form.tsx around lines 41 - 49,
The useEffect that calls form.reset when initialData changes should not depend
on the entire form object because form's reference can change after reset and
trigger a render loop; update the dependency array to reference only initialData
and the specific reset function (form.reset) instead of form, i.e., keep
useEffect dependent on initialData and reset to ensure the effect runs correctly
without risking infinite re-renders.
app/(afterLogin)/prompt/_components/prompt-form-skeleton.tsx (1)

5-5: mx-auto 클래스가 중복 선언되어 있습니다.

layout 변수에서 mx-auto가 두 번 나타납니다. 기능상 문제는 없지만, 가독성을 위해 정리하는 것이 좋습니다.

🔧 수정 제안
-  const layout = cn("mx-auto max-w-7xl lg:px-0 px-5 min-w-90 mx-auto");
+  const layout = cn("mx-auto max-w-7xl lg:px-0 px-5 min-w-90");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/_components/prompt-form-skeleton.tsx at line 5, The
class list assigned to the layout constant contains a duplicated "mx-auto";
update the layout declaration (the cn(...) call that defines layout) to remove
the redundant "mx-auto" so the class string becomes deduplicated (e.g., only one
"mx-auto" present) while keeping the other classes intact for spacing and width
behavior.
app/(afterLogin)/prompt/(creat-or-update)/[id]/edit/page.tsx (1)

1-1: "use client" 선언이 필요한 이유를 주석으로 명시하면 좋습니다.

use() 훅 사용으로 인해 클라이언트 컴포넌트가 필요하다는 점을 간단히 주석으로 남기면 향후 유지보수에 도움이 됩니다.

// use() hook requires client component
"use client";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/(creat-or-update)/[id]/edit/page.tsx at line 1, Add
a one-line comment immediately above the "use client" directive explaining that
this file is a client component because it uses the use() hook (e.g., note that
use() requires a client component), so future maintainers understand why "use
client" is present in edit/page.tsx; keep the comment short and adjacent to the
"use client" line.
app/(afterLogin)/prompt/(creat-or-update)/write/page.tsx (1)

3-5: 페이지 메타데이터 추가를 고려해보세요.

SEO와 브라우저 탭 제목을 위해 metadata export를 추가하면 좋습니다.

🔧 예시
export const metadata = {
  title: "프롬프트 작성",
  description: "새로운 프롬프트를 작성합니다.",
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/(creat-or-update)/write/page.tsx around lines 3 - 5,
Add a metadata export for the page to provide SEO and tab title info: export a
constant named metadata (e.g., export const metadata = { title: "프롬프트 작성",
description: "새로운 프롬프트를 작성합니다." }) alongside the default PromptWrite component
so the page (PromptWrite) and its rendered PromptForm have appropriate
title/description metadata.
app/(afterLogin)/prompt/board-navigate.tsx (1)

12-13: 수정 화면 판별은 경로 끝 기준으로 좁혀 두는 편이 안전합니다.

지금 방식은 /edit가 중간에만 들어가도 수정 화면으로 판단합니다. 현재 lib/routes.ts의 EDIT 패턴이 /prompt/:id/edit라면 endsWith("/edit")나 세그먼트 기준 체크가 더 명확합니다.

♻️ 제안 코드
-  const isEditPage = pathname.includes("/edit");
+  const isEditPage = pathname.endsWith("/edit");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/board-navigate.tsx around lines 12 - 13, The current
isEditPage check using pathname.includes("/edit") is too broad; update the logic
in the component that calls usePathname (the const isEditPage definition) to
detect the edit screen more precisely by checking the path end (e.g.,
pathname.endsWith("/edit")) or by splitting pathname into segments and verifying
the last segment equals "edit" to match the EDIT route pattern used by
lib/routes.ts (e.g., /prompt/:id/edit).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(afterLogin)/prompt/board-navigate.tsx:
- Around line 17-19: The handleGoBack currently calls router.back() directly
which can misbehave when browser history is empty; update the handleGoBack
function to check window.history.length and call router.back() only if history
length > 1, otherwise perform a safe fallback navigation via router.replace to a
stable route (e.g., '/board' or the list page); locate the handleGoBack function
and the router.back() usage and implement the history-length guard and
router.replace fallback to ensure reliable back navigation.

In `@app/`(afterLogin)/prompt/layout.tsx:
- Around line 9-12: The outer wrapper div currently has className "w-full
bg-gray-50" which lets the background stop when child content is short; update
the wrapper's className to include min-h-screen (e.g., "w-full min-h-screen
bg-gray-50") so the background covers the full viewport height while keeping
Header and {children} intact.

---

Duplicate comments:
In `@app/`(afterLogin)/prompt/_components/prompt-form.tsx:
- Around line 100-107: The CLSBox component is wrapping its children with a <p>
which causes invalid nested <p><p>...</p></p> markup when callers (e.g., where
form.formState.errors.category is rendered) already pass a <p>; update CLSBox to
use a non-paragraph wrapper (e.g., <div>) around its children to avoid nested
paragraph tags; locate CLSBox usage in prompt-form.tsx (the calls around the
category and error renderers) and ensure the changed wrapper fixes all
occurrences (also at the other similar call sites noted) so no callers need to
change.

---

Nitpick comments:
In `@app/`(afterLogin)/prompt/_components/prompt-form-skeleton.tsx:
- Line 5: The class list assigned to the layout constant contains a duplicated
"mx-auto"; update the layout declaration (the cn(...) call that defines layout)
to remove the redundant "mx-auto" so the class string becomes deduplicated
(e.g., only one "mx-auto" present) while keeping the other classes intact for
spacing and width behavior.

In `@app/`(afterLogin)/prompt/_components/prompt-form.tsx:
- Line 29: The class string assigned to the variable layout contains a
duplicated "mx-auto" ("const layout = cn(\"mx-auto max-w-7xl lg:px-0 px-5
min-w-90 mx-auto\");"); remove the redundant "mx-auto" so the cn(...) call only
includes it once (match the skeleton component's spacing classes) to avoid
duplicate class declarations.
- Around line 41-49: The useEffect that calls form.reset when initialData
changes should not depend on the entire form object because form's reference can
change after reset and trigger a render loop; update the dependency array to
reference only initialData and the specific reset function (form.reset) instead
of form, i.e., keep useEffect dependent on initialData and reset to ensure the
effect runs correctly without risking infinite re-renders.

In `@app/`(afterLogin)/prompt/(creat-or-update)/[id]/edit/page.tsx:
- Line 1: Add a one-line comment immediately above the "use client" directive
explaining that this file is a client component because it uses the use() hook
(e.g., note that use() requires a client component), so future maintainers
understand why "use client" is present in edit/page.tsx; keep the comment short
and adjacent to the "use client" line.

In `@app/`(afterLogin)/prompt/(creat-or-update)/write/page.tsx:
- Around line 3-5: Add a metadata export for the page to provide SEO and tab
title info: export a constant named metadata (e.g., export const metadata = {
title: "프롬프트 작성", description: "새로운 프롬프트를 작성합니다." }) alongside the default
PromptWrite component so the page (PromptWrite) and its rendered PromptForm have
appropriate title/description metadata.

In `@app/`(afterLogin)/prompt/board-navigate.tsx:
- Around line 12-13: The current isEditPage check using
pathname.includes("/edit") is too broad; update the logic in the component that
calls usePathname (the const isEditPage definition) to detect the edit screen
more precisely by checking the path end (e.g., pathname.endsWith("/edit")) or by
splitting pathname into segments and verifying the last segment equals "edit" to
match the EDIT route pattern used by lib/routes.ts (e.g., /prompt/:id/edit).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c15f895f-4a65-402c-a23e-b6711524b723

📥 Commits

Reviewing files that changed from the base of the PR and between 6d3f848 and a542110.

📒 Files selected for processing (8)
  • app/(afterLogin)/prompt/(creat-or-update)/[id]/edit/edit-prompt-form.tsx
  • app/(afterLogin)/prompt/(creat-or-update)/[id]/edit/page.tsx
  • app/(afterLogin)/prompt/(creat-or-update)/layout.tsx
  • app/(afterLogin)/prompt/(creat-or-update)/write/page.tsx
  • app/(afterLogin)/prompt/_components/prompt-form-skeleton.tsx
  • app/(afterLogin)/prompt/_components/prompt-form.tsx
  • app/(afterLogin)/prompt/board-navigate.tsx
  • app/(afterLogin)/prompt/layout.tsx

Comment on lines +17 to +19
const handleGoBack = () => {
router.back();
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# 파일 내용 확인
cat -n app/\(afterLogin\)/prompt/board-navigate.tsx

Repository: Inflearn-Prog/Prog-fe

Length of output: 1346


🏁 Script executed:

# 파일 위치 확인 및 주변 구조 파악
fd "board-navigate" --type f

Repository: Inflearn-Prog/Prog-fe

Length of output: 107


🌐 Web query:

Next.js App Router router.back() behavior new tab refresh fallback

💡 Result:

In the Next.js App Router, router.back() is just “go to the previous entry in the browser’s history stack” (i.e., it relies on the browser History API), and Next.js does not provide a built-in fallback for cases where there’s nothing to go back to. [1]

Because of that, when a user opens a page in a new tab (or lands directly on a URL), there may be no previous history entry for that tab/session—so a “back” call will effectively do nothing (this is standard history.back() behavior). [2] In other cases, if there is a previous entry but it’s external, “back” may take them out of your app.

Common pattern: “back if possible, else go to a safe route”

'use client'
import { useRouter } from 'next/navigation'

export function BackButton({ fallback = '/' }: { fallback?: string }) {
  const router = useRouter()

  return (
    <button
      onClick={() => {
        if (window.history.length > 1) router.back()
        else router.replace(fallback)
      }}
    >
      Back
    </button>
  )
}

This approach (using window.history.length to decide) is commonly recommended because other heuristics (like history.state.idx) have proven unreliable across time/versions. [3]

Sources: Next.js useRouter (router.back) docs [1], MDN history.back() behavior when no previous entry exists [2], Stack Overflow fallback check pattern [3].


router.back() 단독 사용은 불안정한 뒤로가기 동작을 초래합니다.

게시글 작성/수정은 중요한 내비게이션이라 안정성이 필수입니다. 새 탭 진입이나 새로고침 후에는 브라우저 히스토리가 비어있을 수 있어 back()이 작동하지 않거나, 외부 페이지로 복귀할 수 있습니다.

Next.js App Router의 router.back()은 기본 브라우저 History API일 뿐이며, fallback을 제공하지 않습니다. 다음과 같이 history 길이를 확인한 후 안전한 fallback route를 제시하세요:

추천 구현 패턴
const handleGoBack = () => {
  if (window.history.length > 1) {
    router.back();
  } else {
    router.replace('/board'); // 또는 적절한 목록 페이지
  }
};

window.history.length는 브라우저 히스토리 스택 길이를 확인하는 가장 안정적인 방법입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/board-navigate.tsx around lines 17 - 19, The
handleGoBack currently calls router.back() directly which can misbehave when
browser history is empty; update the handleGoBack function to check
window.history.length and call router.back() only if history length > 1,
otherwise perform a safe fallback navigation via router.replace to a stable
route (e.g., '/board' or the list page); locate the handleGoBack function and
the router.back() usage and implement the history-length guard and
router.replace fallback to ensure reliable back navigation.

Comment on lines +9 to +12
<div className="w-full bg-gray-50">
<Header />
{children}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

래퍼에 min-h-screen을 추가해 배경이 끊기지 않게 해주세요.

지금은 컨테이너 높이가 자식 콘텐츠만큼만 잡혀서, 내용이 짧은 화면에서는 viewport 하단이 bg-gray-50이 아니라 body 배경색으로 보일 수 있습니다. app/(afterLogin)/(dashboard)/layout.tsx도 같은 역할의 레이아웃에서 min-h-screen을 쓰고 있으니 여기서도 맞춰두면 화면 일관성이 좋아집니다. 작은 차이인데 이런 게 은근 UX를 살립니다.

🔧 제안 diff
-    <div className="w-full bg-gray-50">
+    <div className="w-full min-h-screen bg-gray-50">
📝 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
<div className="w-full bg-gray-50">
<Header />
{children}
</div>
<div className="w-full min-h-screen bg-gray-50">
<Header />
{children}
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/layout.tsx around lines 9 - 12, The outer wrapper
div currently has className "w-full bg-gray-50" which lets the background stop
when child content is short; update the wrapper's className to include
min-h-screen (e.g., "w-full min-h-screen bg-gray-50") so the background covers
the full viewport height while keeping Header and {children} intact.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/(afterLogin)/prompt/[id]/_components/prompt-detail-content.tsx (1)

20-25: useSuspenseQuery 사용이 적절합니다.

Suspense와 함께 스트리밍 방식의 데이터 페칭을 잘 활용하고 있습니다. 다만 Line 25의 변수 재할당은 불필요해 보입니다.

♻️ 변수 재할당 제거 제안
 export function PromptDetailContent({ id, user }: PromptDetailContentProps) {
-  const { data } = useSuspenseQuery({
+  const { data: prompt } = useSuspenseQuery({
     ...promptQueries.detail(id),
   });
-
-  const prompt = data;

   return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(afterLogin)/prompt/[id]/_components/prompt-detail-content.tsx around
lines 20 - 25, The variable reassignment "const prompt = data;" in
PromptDetailContent is redundant; remove that line and use "data" directly (or
rename the function param destructure to "data: prompt" if you prefer a semantic
name) so all references use the existing useSuspenseQuery result instead of
creating an unnecessary alias; update any downstream uses in PromptDetailContent
to reference "data" (or the chosen alias) and delete the redundant "prompt"
declaration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/`(afterLogin)/prompt/[id]/_components/prompt-detail-content.tsx:
- Around line 20-25: The variable reassignment "const prompt = data;" in
PromptDetailContent is redundant; remove that line and use "data" directly (or
rename the function param destructure to "data: prompt" if you prefer a semantic
name) so all references use the existing useSuspenseQuery result instead of
creating an unnecessary alias; update any downstream uses in PromptDetailContent
to reference "data" (or the chosen alias) and delete the redundant "prompt"
declaration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ab881bc8-80f2-44d2-8471-1f0b09f549b0

📥 Commits

Reviewing files that changed from the base of the PR and between a542110 and dd39677.

📒 Files selected for processing (3)
  • app/(afterLogin)/prompt/[id]/_components/post-detail-skeleton.tsx
  • app/(afterLogin)/prompt/[id]/_components/prompt-detail-content.tsx
  • app/(afterLogin)/prompt/[id]/page.tsx

Copy link
Copy Markdown
Contributor

@fryzke fryzke left a comment

Choose a reason for hiding this comment

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

코드가 전체적으로 깔끔하고 좋았습니다!! 작업량이 보통 많은 게 아니었을텐데 정말 수고 많으셨습니다 ㅜㅜ
지금 단 코멘트 외에는 더 첨부할 것 없을 거 같습니다. 나중에 명세서 반영되면 알려주세요~


/** 특정 댓글의 대댓글 목록 반환 */
const getRepliesForComment = (commentId: string): PromptCommentResponse[] =>
replies.filter((r) => r.parentId === commentId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

대댓글 매핑 시 O(N^2) 연산을 피할 수 있는 구조를 고민해봐도 좋을 것 같습니다.
댓글이 수백개가 되면 렌더링 시 부담이 될 수도 있을 것 같아요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

대댓글 매핑 시 O(N^2) 연산을 피할 수 있는 구조를 고민해봐도 좋을 것 같습니다. 댓글이 수백개가 되면 렌더링 시 부담이 될 수도 있을 것 같아요.

좋은 피드백인거 같습니다.

하지만 지금 서비스에서 댓글로 인한 과부화는 걸리지 않을걸로 예상됩니다.

해당 건은 추후 안정화 이후 개선하는게 좋을거 같습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 새로운 기능 구현

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

[Feature] 게시글 CRUD

2 participants