Skip to content

[CCI-37] feat : API 베이스 마련 및 각종 API 연결#1

Merged
jpark0506 merged 9 commits intomainfrom
feat/CCI-37
Jun 1, 2025
Merged

[CCI-37] feat : API 베이스 마련 및 각종 API 연결#1
jpark0506 merged 9 commits intomainfrom
feat/CCI-37

Conversation

@jpark0506
Copy link
Contributor

@jpark0506 jpark0506 commented Jun 1, 2025

📌 작업 내용

  • API fetch를 위한 인스턴스 생성
  • API 연결
  • 자소서 추가, 자소서 리스트 조회, 상세 조회
  • 이력서 추가, 이력서 리스트 조회
  • 면접 생성

📎 관련 이슈

--> CCI-37

Summary by CodeRabbit

  • New Features

    • Workspace now supports dynamic fetching, uploading, and detailed viewing of resumes and cover letters, replacing static mock data.
    • Added dialogs for uploading resumes (PDF) and creating cover letters, with progress indication and error handling.
    • Cover letters now use separate corporate and job name fields, and display detailed Q&A in previews.
    • Integrated member session management with login/logout and session persistence.
    • Interview creation flow enhanced with backend integration for resume and cover letter selection, and expanded AI voice options.
    • Added AI-powered interview question generation feature.
    • Added utility for formatting dates and custom error handling for API requests.
  • Bug Fixes

    • Improved error handling and validation throughout document management and interview creation flows.
  • Chores

    • Added comprehensive API client modules and type definitions for resumes, cover letters, interviews, and AI-generated interview questions.
    • Introduced mock data for development and testing purposes.

@jpark0506 jpark0506 requested a review from Copilot June 1, 2025 07:12
@jpark0506 jpark0506 self-assigned this Jun 1, 2025
@jpark0506 jpark0506 added the enhancement New feature or request label Jun 1, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 1, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This update introduces comprehensive backend integration for resume, cover letter, and interview management. It adds API client modules, DTO type definitions, and utility functions for error handling and HTTP requests. The workspace and interview creation UIs are refactored to fetch, display, and mutate data using these APIs. Member session management, login/logout, and detailed document dialogs are implemented, replacing mock data with live data flows.

Changes

File(s) Change Summary
api/ai-interview.ts New module for AI interview question generation API function.
api/api-test.ts New script for sequentially testing resume, cover letter, interview, and AI APIs.
api/coverletter.ts New API client for creating, retrieving, and listing cover letters.
api/interview.ts New API client for interview creation, participation, status updates, termination, and listing.
api/resume.ts New API client for presigned upload URL, saving resume metadata, and fetching resume details/list.
api/types/coverletter-types.ts New TypeScript interfaces for cover letter DTOs and API responses.
api/types/interview-types.ts New TypeScript interfaces for interview-related DTOs and API responses.
api/types/resume-types.ts New TypeScript interfaces for resume DTOs and API responses.
app/login/page.tsx Login page now uses session context; login button triggers login and redirects.
app/providers.tsx Providers component wraps children with new member session context provider.
app/workspace/page.tsx Workspace refactored for API-driven resume/cover letter listing, filtering, uploading, detailed dialogs, and UI improvements.
components/cover-letter-form.tsx Cover letter form refactored: title split into corporate/job name, new submission structure, validation, and preview.
components/interview/steps/Step2.tsx Interview step 2 refactored for backend-driven resume/cover letter selection, upload/creation dialogs, and detailed previews.
components/interview/steps/Step3.tsx AI voice selection expanded to eight gender/age options with updated labels and icons.
components/member-session-context.tsx New React context/provider for member session, with login/logout and session enforcement hooks.
components/ui/button-logout.tsx New logout button component using session context and redirect.
hooks/useInterviewWizard.ts Interview wizard submission now validates, transforms, and submits typed payloads with improved error handling.
mock/exCoverLetters.ts, mock/exResumes.ts New mock data files for cover letters and resumes (now superseded by API-driven data).
utils/date/convertDate.ts New utility for formatting dates as relative or absolute strings.
utils/error/error.ts New custom error classes for HTTP error handling.
utils/fetch/fetch.ts New generic fetch utility with server/AI clients, supporting error handling and all HTTP verbs.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WorkspacePage
    participant APIClient (resume/coverletter)
    participant Server

    User->>WorkspacePage: Loads workspace
    WorkspacePage->>APIClient: fetch resume list (memberId)
    WorkspacePage->>APIClient: fetch cover letter list (memberId)
    APIClient->>Server: GET /resumes/, /coverletters/me
    Server-->>APIClient: Resume/Cover letter data
    APIClient-->>WorkspacePage: Lists
    WorkspacePage->>User: Shows document cards

    User->>WorkspacePage: Clicks "Upload Resume"
    WorkspacePage->>ResumeUploadDialog: Opens dialog
    User->>ResumeUploadDialog: Selects file, clicks upload
    ResumeUploadDialog->>APIClient: getPresignedUploadUrl(fileName)
    APIClient->>Server: GET /resumes/upload
    Server-->>APIClient: Presigned URL
    APIClient-->>ResumeUploadDialog: URL
    ResumeUploadDialog->>Server: Uploads file to S3 via presigned URL
    ResumeUploadDialog->>APIClient: saveResume(metadata)
    APIClient->>Server: POST /resumes/upload
    Server-->>APIClient: Save result
    APIClient-->>ResumeUploadDialog: Result
    ResumeUploadDialog->>WorkspacePage: onSuccess (refresh list)
Loading
sequenceDiagram
    participant User
    participant CoverLetterForm
    participant APIClient
    participant Server

    User->>CoverLetterForm: Fills form, submits
    CoverLetterForm->>APIClient: createCoverletter(data)
    APIClient->>Server: POST /coverletters
    Server-->>APIClient: Response
    APIClient-->>CoverLetterForm: Result
    CoverLetterForm->>User: Shows success/updates list
Loading
sequenceDiagram
    participant User
    participant Step2
    participant APIClient
    participant Server

    User->>Step2: Opens interview step 2
    Step2->>APIClient: Fetch resumes/cover letters
    APIClient->>Server: GET /resumes/, /coverletters/me
    Server-->>APIClient: Data
    APIClient-->>Step2: Lists
    User->>Step2: Selects resume/cover letter
    Step2->>APIClient: Fetch details (optional)
    APIClient->>Server: GET /resumes/{id}, /coverletters/{id}
    Server-->>APIClient: Detail data
    APIClient-->>Step2: Detail
    Step2->>User: Shows preview
Loading

Possibly related PRs

Poem

🐇
A hop, a leap, new APIs appear,
Resumes and letters, all fetched from here.
Dialogs for uploads, details to show,
Member sessions persist wherever you go.
With voices and badges, the workspace feels bright—
The rabbit’s code garden grows overnight!
🌱✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25e021c and cd07517.

📒 Files selected for processing (1)
  • utils/date/convertDate.ts (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR establishes the API foundation and wires up various endpoints for resumes, cover letters, and interview creation.

  • Introduces shared error classes and date conversion utility
  • Implements member session context and Next.js login/workspace integration
  • Adds API modules, types, and connects them in hooks and pages

Reviewed Changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
utils/error/error.ts Added custom HTTP error classes
utils/date/convertDate.ts Created a human-friendly date formatter
mock/exResumes.ts Seed data for resume list (unused)
mock/exCoverLetters.ts Seed data for cover letter list (unused)
hooks/useInterviewWizard.ts Hook updated to build and submit interview payload
components/ui/button-logout.tsx Logout button wired to session context
components/member-session-context.tsx Provider and hooks for member session
components/interview/steps/Step3.tsx Expanded voice options
components/cover-letter-form.tsx Form updated to include memberId and Q&A payload
app/workspace/page.tsx Workspace page wired to API queries/mutations
app/providers.tsx Added MemberSessionProvider around app
app/login/page.tsx Login page now sets session on temporary login
api/types/*.ts Added DTO definitions for resume, interview, cover letter
api/resume.ts Resume upload/detail/list API calls
api/interview.ts Interview creation and management API calls
api/coverletter.ts Cover letter create/list/detail API calls
api/api-test.ts Test harness for all API functions
api/ai-interview.ts AI interview question generation endpoint wrapper
Comments suppressed due to low confidence (2)

hooks/useInterviewWizard.ts:70

  • This debug log will run in production. Remove or guard it behind a development flag to avoid unnecessary console noise.
console.log("🧪 validate", step, ok, form); // ← 추가

mock/exResumes.ts:1

  • This mock data file isn’t imported anywhere in the application. If it's no longer needed, consider removing it to reduce dead code.
export const resumes = [


export class UnProcessableError extends Error {
constructor(message?: string) {
super(message || "UnProcessable");
Copy link

Copilot AI Jun 1, 2025

Choose a reason for hiding this comment

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

The default error message "UnProcessable" is misspelled/mismatched with the class name. Consider using "Unprocessable" for consistency.

Suggested change
super(message || "UnProcessable");
super(message || "Unprocessable");

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +9
import { useMemberSession } from '../member-session-context'

export function ButtonLogout() {
const { logout } = useMemberSession()
return (
<button
onClick={() => {
logout()
window.location.href = '/login'
Copy link

Copilot AI Jun 1, 2025

Choose a reason for hiding this comment

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

Directly setting window.location.href causes a full reload. Consider using Next.js router.push for smoother client-side navigation.

Suggested change
import { useMemberSession } from '../member-session-context'
export function ButtonLogout() {
const { logout } = useMemberSession()
return (
<button
onClick={() => {
logout()
window.location.href = '/login'
import { useRouter } from 'next/router'
import { useMemberSession } from '../member-session-context'
export function ButtonLogout() {
const { logout } = useMemberSession()
const router = useRouter()
return (
<button
onClick={() => {
logout()
router.push('/login')

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@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: 24

🔭 Outside diff range comments (1)
app/workspace/page.tsx (1)

247-266: 🛠️ Refactor suggestion

doc.type === "file" branch is unreachable

The mapping logic assigns "resume" or "manual" to type, but the menu checks for "file". This means the download action never shows.

-                        {"type" in doc && doc.type === "file" ? (
+                        {doc.type === "resume" ? (

Adjust downstream checks consistently (resume ➜ download, manual ➜ detail) or standardise the enum.

🧹 Nitpick comments (21)
mock/exResumes.ts (1)

1-23: Consider updating mock dates for better realism.

The mock data structure is well-designed and consistent. However, the dates (2023-03-10 to 2023-05-15) are quite old relative to the current time, which might make the mock data feel outdated during development and testing.

Consider updating the dates to more recent values:

 export const resumes = [
   {
     id: 1,
     name: "신입 개발자 이력서.pdf",
-    date: "2023-05-15",
+    date: "2024-12-15",
     size: "1.2MB",
     type: "resume",
   },
   {
     id: 2,
     name: "포트폴리오_2023.pdf",
-    date: "2023-04-22",
+    date: "2024-11-22",
     size: "3.5MB",
     type: "resume",
   },
   {
     id: 3,
     name: "경력기술서_최종.docx",
-    date: "2023-03-10",
+    date: "2024-10-10",
     size: "890KB",
     type: "resume",
   },
 ];
components/ui/button-logout.tsx (2)

7-10: Consider using Next.js router for navigation instead of window.location.href.

Using window.location.href bypasses Next.js client-side routing and causes a full page reload. Consider using Next.js router for better performance and user experience.

+import { useRouter } from 'next/navigation'
+
 export function ButtonLogout() {
     const { logout } = useMemberSession()
+    const router = useRouter()
     return (
         <button
             onClick={() => {
                 logout()
-                window.location.href = '/login'
+                router.push('/login')
             }}

8-8: Add error handling for logout operation.

The logout operation could potentially fail. Consider adding error handling to provide better user feedback.

             onClick={() => {
-                logout()
+                try {
+                    logout()
+                } catch (error) {
+                    console.error('Logout failed:', error)
+                    // Handle error appropriately
+                }
                 router.push('/login')
             }}
api/ai-interview.ts (1)

3-3: Remove or update the TODO comment.

The comment suggests that the endpoint and types need adjustment, but since this is being reviewed for production, either implement the proper types or create a tracking issue.

Would you like me to help create a GitHub issue to track the type definition work, or should these types be implemented now?

mock/exCoverLetters.ts (2)

1-34: Add TypeScript interface for type safety.

Consider defining a TypeScript interface for the cover letter mock data to ensure type consistency and catch potential errors.

+interface CoverLetter {
+  id: number
+  name: string
+  date: string
+  category: 'coverLetter'
+  type: 'file' | 'manual'
+  size?: string
+  questions?: number
+}
+
-export const coverLetters = [
+export const coverLetters: CoverLetter[] = [
   {
     id: 1,

4-5: Consider using ISO date format for consistency.

The dates are in YYYY-MM-DD format which is good, but consider using ISO 8601 format with time for better consistency with typical API responses.

For example: "2023-05-10T00:00:00Z" instead of "2023-05-10"

Also applies to: 12-13, 28-29

components/interview/steps/Step3.tsx (2)

25-32: Consider more diverse sample phrases for better user experience.

All voice options use very similar sample phrases. Consider using more diverse phrases that better demonstrate the different voice characteristics (age/gender).

                     {[
-                        { value: "female20", label: "👩‍🦰 여성(20대)", sample: "안녕하세요! 면접을 시작하겠습니다." },
-                        { value: "female30", label: "👩 여성(30대)", sample: "안녕하세요! 준비되셨나요?" },
-                        { value: "female40", label: "👩‍🦳 여성(40대)", sample: "안녕하세요! 편안하게 답변해주세요." },
-                        { value: "female50", label: "👵 여성(50대)", sample: "안녕하세요! 좋은 면접이 되길 바랍니다." },
+                        { value: "female20", label: "👩‍🦰 여성(20대)", sample: "안녕하세요! 오늘 면접 잘 부탁드려요!" },
+                        { value: "female30", label: "👩 여성(30대)", sample: "반갑습니다. 편안하게 대화해보시죠." },
+                        { value: "female40", label: "👩‍🦳 여성(40대)", sample: "안녕하세요. 차분하게 답변해주세요." },
+                        { value: "female50", label: "👵 여성(50대)", sample: "좋은 시간이 되길 바랍니다." },

25-32: Consider accessibility improvements for emoji usage.

The emoji icons provide visual distinction but may not be accessible to screen readers. Consider adding aria-labels or additional text descriptions.

                             <CardContent className="p-4 text-center">
-                                <h3 className="font-semibold mb-2">{voice.label}</h3>
+                                <h3 className="font-semibold mb-2" aria-label={voice.label.replace(/👩‍🦰|👩|👩‍🦳|👵|👨‍🦱|👨|👨‍🦳|👴/g, '')}>{voice.label}</h3>
api/api-test.ts (1)

21-26: Use dynamic test data instead of hardcoded values.

The hardcoded fileUrl and other test values may not accurately reflect real usage scenarios and could cause test failures.

Consider generating dynamic test data:

const resumeResult = await saveResume({
  memberId: 1,
- fileName: "test.pdf",
- fileUrl: "https://example.com/test.pdf",
- fileSize: 12345,
+ fileName: `test-${Date.now()}.pdf`,
+ fileUrl: presigned.data?.uploadUrl || "https://example.com/test.pdf",
+ fileSize: Math.floor(Math.random() * 100000) + 1000,
});
utils/error/error.ts (1)

1-20: Consider adding more HTTP error classes and standardizing constructor patterns.

The current implementation covers basic error cases but could be expanded for better error handling coverage.

Consider adding more error types and standardizing the constructor pattern:

+ export abstract class HttpError extends Error {
+   public readonly statusCode: number;
+   public readonly context?: Record<string, any>;
+   
+   constructor(statusCode: number, message: string, context?: Record<string, any>) {
+     super(message);
+     this.statusCode = statusCode;
+     this.context = context;
+     this.name = this.constructor.name;
+   }
+ }

- export class BadRequestError extends Error {
+ export class BadRequestError extends HttpError {
-   constructor(message?: string) {
-     super(message || "Bad Request");
-     this.name = "BadRequestError";
+   constructor(message?: string, context?: Record<string, any>) {
+     super(400, message || "Bad Request", context);
   }
}

+ export class UnauthorizedError extends HttpError {
+   constructor(message?: string, context?: Record<string, any>) {
+     super(401, message || "Unauthorized", context);
+   }
+ }
+ 
+ export class InternalServerError extends HttpError {
+   constructor(message?: string, context?: Record<string, any>) {
+     super(500, message || "Internal Server Error", context);
+   }
+ }
utils/fetch/fetch.ts (1)

22-22: Remove or make logging configurable.

Console logging in production code can be problematic for performance and security. Consider making it configurable or removing it.

+ interface FetchConfig {
+   baseURL: string;
+   debug?: boolean;
+ }

- function createFetch(baseURL: string) {
+ function createFetch(config: FetchConfig | string) {
+   const { baseURL, debug } = typeof config === 'string' 
+     ? { baseURL: config, debug: false } 
+     : config;

  const get = async <T>(/*...*/) => {
-   console.log("GET : ", url);
+   if (debug) console.log("GET : ", url);
    // ...
  };

Also applies to: 54-54, 99-99

components/member-session-context.tsx (1)

5-9: Consider adding session expiration and additional context data.

The current session context is minimal. Consider adding session expiration and user metadata for better session management.

interface MemberSessionContextProps {
  memberId: number | null
+ memberData?: { 
+   name?: string; 
+   email?: string; 
+   role?: string;
+ } | null
+ sessionExpiry?: Date | null
+ isSessionValid: boolean
  login: (id: number) => void
+ loginWithData: (id: number, data: any, expiryHours?: number) => void
  logout: () => void
+ refreshSession: () => Promise<void>
}

This would allow for richer session management including automatic session validation and user metadata caching.

hooks/useInterviewWizard.ts (1)

76-77: Consider using more specific error types.

While the error message is user-friendly, consider creating specific error types for better error handling and debugging.

Example implementation:

class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

if (!form.resumeId || !form.coverLetterId) {
  throw new ValidationError("이력서/자기소개서가 선택되지 않았습니다.");
}
api/resume.ts (2)

34-34: Remove trailing slash from the endpoint URL.

The endpoint /resumes/ has a trailing slash which might cause issues depending on the server configuration. For consistency with other endpoints, remove the trailing slash.

Apply this diff:

-  return serverFetch.get<ApiResponseResumeListDTO>("/resumes/", { memberId });
+  return serverFetch.get<ApiResponseResumeListDTO>("/resumes", { memberId });

12-14: Consider adding error handling for API calls.

While the functions are clean, they don't include explicit error handling. Consider wrapping these calls in try-catch blocks or documenting that error handling should be done by the caller.

Would you like me to generate a wrapper utility that adds consistent error handling and logging for all API calls? This could help standardize error handling across the application.

Also applies to: 27-29, 34-34

components/cover-letter-form.tsx (1)

62-62: Consider adding null check for memberId.

While useRequireMemberId should handle authentication, adding an explicit check would improve defensive programming.

  const handleSubmit = () => {
+   if (!memberId) {
+     console.error('Member ID is required');
+     return;
+   }
    onSubmit({
      memberId: memberId!,
      corporateName,
      jobName,
      qnaDTOList: questionAnswerPairs.map(({ question, answer }) => ({ question, answer })),
    })
  }
api/types/resume-types.ts (1)

38-38: Consider expanding supported file types.

The fileType is currently restricted to only "PDF". This might be limiting if the application needs to support other resume formats in the future.

Consider making it more flexible:

- fileType?: "PDF";
+ fileType?: "PDF" | "DOC" | "DOCX";

Or if PDF-only is a business requirement, consider adding a comment explaining this restriction:

- fileType?: "PDF";
+ fileType?: "PDF"; // Currently only PDF format is supported
api/types/interview-types.ts (1)

75-80: DTO interface names deviate from PascalCase convention

changeMemberStatusDTO (and endInterviewRequestDTO below) start with a lower-case letter, breaking the otherwise consistent PascalCase naming in this file.

-export interface changeMemberStatusDTO {
+export interface ChangeMemberStatusDTO {
   memberId: number;
   status: "NO_SHOW" | "SCHEDULED" | "IN_PROGRESS" | "DONE";
 }

(Apply the same treatment to endInterviewRequestDTOEndInterviewRequestDTO.)

components/interview/steps/Step2.tsx (2)

254-272: File-upload XHR lacks abort handling & leaks if the dialog closes

If the user closes the dialog mid-upload or the component unmounts, the XMLHttpRequest keeps running, potentially causing state updates on an unmounted component and wasted bandwidth.

A minimal, self-contained fix:

-const xhr = new XMLHttpRequest()
+const xhr = new XMLHttpRequest()
+// Allow cancelling when the component unmounts
+const abort = () => { try { xhr.abort() } catch (_) {} }
+// Cleanup
+window.addEventListener('beforeunload', abort)
+// ...
+xhr.onload = () => {
+  window.removeEventListener('beforeunload', abort)
+  // existing logic …
+}
+xhr.onerror = () => {
+  window.removeEventListener('beforeunload', abort)
+  // existing logic …
+}

Alternatively, switch to fetch with an AbortController; you already lose progress events for fetch so the current approach is acceptable if the abort is addressed.

🧰 Tools
🪛 Biome (1.9.4)

[error] 258-259: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


80-89: coverLetterSchema is declared but never used

Dead code increases bundle size and confuses future readers. If you intend to validate the dialog’s form, wire this schema into zodResolver & react-hook-form; otherwise, please remove it.

app/workspace/page.tsx (1)

397-468: Duplicated resume-upload component across files – extract to a shared module

ResumeUploadDialog here is virtually identical to the one in Step2.tsx. Duplication invites drift and double maintenance when S3 logic changes. Export a single <ResumeUploadDialog> component from components/resume/resume-upload-dialog.tsx and reuse it in both places.

Benefits:
• one-time bug fixes
• lighter bundle (tree-shakable)
• consistent UX

🧰 Tools
🪛 Biome (1.9.4)

[error] 426-428: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf28296 and f272f8b.

📒 Files selected for processing (22)
  • api/ai-interview.ts (1 hunks)
  • api/api-test.ts (1 hunks)
  • api/coverletter.ts (1 hunks)
  • api/interview.ts (1 hunks)
  • api/resume.ts (1 hunks)
  • api/types/coverletter-types.ts (1 hunks)
  • api/types/interview-types.ts (1 hunks)
  • api/types/resume-types.ts (1 hunks)
  • app/login/page.tsx (2 hunks)
  • app/providers.tsx (1 hunks)
  • app/workspace/page.tsx (9 hunks)
  • components/cover-letter-form.tsx (3 hunks)
  • components/interview/steps/Step2.tsx (2 hunks)
  • components/interview/steps/Step3.tsx (1 hunks)
  • components/member-session-context.tsx (1 hunks)
  • components/ui/button-logout.tsx (1 hunks)
  • hooks/useInterviewWizard.ts (3 hunks)
  • mock/exCoverLetters.ts (1 hunks)
  • mock/exResumes.ts (1 hunks)
  • utils/date/convertDate.ts (1 hunks)
  • utils/error/error.ts (1 hunks)
  • utils/fetch/fetch.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (10)
app/login/page.tsx (1)
components/member-session-context.tsx (1)
  • useMemberSession (39-43)
components/ui/button-logout.tsx (1)
components/member-session-context.tsx (1)
  • useMemberSession (39-43)
app/providers.tsx (1)
components/member-session-context.tsx (1)
  • MemberSessionProvider (13-37)
api/ai-interview.ts (1)
utils/fetch/fetch.ts (1)
  • aiFetch (135-135)
api/api-test.ts (4)
api/resume.ts (2)
  • getPresignedUploadUrl (11-15)
  • saveResume (18-23)
api/coverletter.ts (3)
  • createCoverletter (10-15)
  • getCoverletterDetail (18-22)
  • findMyCoverletter (25-29)
api/interview.ts (3)
  • createMemberInterview (22-30)
  • changeParticipantsStatus (33-41)
  • terminateInterview (44-52)
api/ai-interview.ts (1)
  • generateInterviewQuestions (4-6)
hooks/useInterviewWizard.ts (1)
api/types/interview-types.ts (1)
  • InterviewCreateDTO (1-30)
utils/fetch/fetch.ts (2)
utils/error/error.ts (3)
  • BadRequestError (1-6)
  • NotFoundError (8-13)
  • UnProcessableError (15-20)
lib/api.ts (1)
  • post (1-15)
api/coverletter.ts (2)
api/types/coverletter-types.ts (4)
  • CreateCoverletterDTO (6-11)
  • ApiResponseCreateCoverletterDTO (13-18)
  • ApiResponseCoverletterDetailDTO (33-38)
  • ApiResponseMyCoverletterListDTO (51-56)
utils/fetch/fetch.ts (1)
  • serverFetch (135-135)
api/resume.ts (2)
utils/fetch/fetch.ts (1)
  • serverFetch (135-135)
api/types/resume-types.ts (5)
  • ApiResponsePresignedUploadDTO (26-31)
  • ResumeCreateDTO (1-6)
  • ApiResponseCreateResumeResultDTO (13-18)
  • ApiResponseResumeDetailDTO (43-48)
  • ApiResponseResumeListDTO (60-65)
api/interview.ts (2)
api/types/interview-types.ts (8)
  • InterviewCreateDTO (1-30)
  • ApiResponseInterviewCreateResultDTO (37-42)
  • CreateMemberInterviewDTO (57-61)
  • ApiResponseCreateMemberInterviewDTO (63-68)
  • changeMemberStatusDTO (75-78)
  • ApiResponseMemberInterviewStatusDTO (80-85)
  • endInterviewRequestDTO (93-95)
  • ApiResponseInterviewEndResultDTO (97-102)
utils/fetch/fetch.ts (1)
  • serverFetch (135-135)
🪛 Biome (1.9.4)
components/interview/steps/Step2.tsx

[error] 258-259: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

app/workspace/page.tsx

[error] 426-428: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (17)
app/providers.tsx (1)

9-9: LGTM! Proper integration of session management.

The MemberSessionProvider is correctly imported and integrated into the provider hierarchy. The nesting within QueryClientProvider ensures that session context is available throughout the application while maintaining proper provider ordering.

Also applies to: 17-19

components/interview/steps/Step3.tsx (1)

38-38:

Details

❓ Verification inconclusive

Remove unsafe type casting and ensure proper typing.

The as any type assertion bypasses TypeScript's type checking and should be avoided. Ensure the voice values match the expected voiceType type definition.

-                            onClick={() => patch({ voiceType: voice.value as any })}
+                            onClick={() => patch({ voiceType: voice.value })}

Please verify that the voice values (female20, male30, etc.) match the expected voiceType enum or union type definition:


🏁 Script executed:

#!/bin/bash
# Search for voiceType type definition to verify allowed values
ast-grep --pattern 'voiceType: $_'
rg -A 5 -B 5 "voiceType.*=" 

Length of output: 1249


🏁 Script executed:

#!/bin/bash
# Show the voices array definition and related types in Step3.tsx
rg -n "const voices" -A 10 -B 5 components/interview/steps/Step3.tsx
# Search for a VoiceType type or enum definition elsewhere in the repo
rg -n "type VoiceType" || rg -n "enum VoiceType"

Length of output: 120


🏁 Script executed:

#!/bin/bash
# 1. Find where the useInterview hook is defined and show its signature
rg -n "export function useInterview" -g "*.ts" -g "*.tsx" -A10

# 2. Look for any interface/type definitions that include a voiceType property
rg -n "interface .*{[^}]*voiceType" -g "*.ts" -g "*.tsx" -A5
rg -n "type .* =.*voiceType" -g "*.ts" -g "*.tsx" -A5

Length of output: 4071


#!/bin/bash
# Locate initialFormState definition to inspect default values including voiceType
rg -n "initialFormState" -A5 -B5 hooks/useInterviewWizard.ts

# Locate the InterviewFormState type or interface to see the voiceType property type
rg -n "InterviewFormState" -A5 -B5 hooks/useInterviewWizard.ts

Ensure proper typing for voiceType before removing the cast

I wasn’t able to locate a voiceType type or enum in the codebase—please confirm that your form state defines voiceType as a union including the new values ("female20", "male30", etc.). Once that’s in place, you can safely remove the unsafe assertion:

- onClick={() => patch({ voiceType: voice.value as any })}
+ onClick={() => patch({ voiceType: voice.value })}

• Verify InterviewFormState in hooks/useInterviewWizard.ts lists all voice options.
• Update the type definition if needed to include the new values.
• Then drop the as any cast in components/interview/steps/Step3.tsx.

hooks/useInterviewWizard.ts (1)

8-9: LGTM! Clean import organization.

The import changes correctly reflect the new centralized API structure, improving modularity and maintainability.

api/coverletter.ts (2)

1-30: Well-structured API client module!

The module follows a clean and consistent pattern with proper TypeScript typing and clear function naming. The use of typed DTOs ensures type safety across the application.


25-29:

Details

✅ Verification successful

Verify the query parameter structure for GET requests.

The current implementation passes memberId as the second parameter to serverFetch.get(). Ensure that the serverFetch utility correctly handles this as a query parameter.


🏁 Script executed:

#!/bin/bash
# Description: Verify how serverFetch handles query parameters for GET requests

# Search for the serverFetch implementation to understand parameter handling
ast-grep --pattern 'const serverFetch = {
  $$$
  get($$$) {
    $$$
  }
  $$$
}'

# Also check for any usage patterns of serverFetch.get with parameters
rg -A 5 'serverFetch\.get.*\(' --glob '*.{ts,tsx,js,jsx}'

Length of output: 1163


To confirm how query parameters are applied, let’s inspect the serverFetch implementation:


🏁 Script executed:

#!/bin/bash
# Locate and display the serverFetch implementation to verify how `get` handles query params
file=$(fd serverFetch.ts 2>/dev/null || fd serverFetch.js 2>/dev/null)
echo "Found serverFetch at: $file"
sed -n '1,200p' "$file"

Length of output: 146


Let’s locate the serverFetch implementation to confirm how .get() constructs query parameters:


🏁 Script executed:

#!/bin/bash
# Search for exported serverFetch definition
file=$(rg -lP "export\s+const\s+serverFetch\s*=" . | head -1)

# Fallback to non-exported const if not found
if [[ -z "$file" ]]; then
  file=$(rg -lP "const\s+serverFetch\s*=" . | head -1)
fi

if [[ -n "$file" ]]; then
  echo "Found serverFetch in: $file"
  sed -n '1,200p' "$file"
else
  echo "⚠️ serverFetch implementation not found by patterns; please verify manually."
fi

Length of output: 3660


Query parameters are handled correctly by serverFetch.get

The serverFetch.get method serializes its optional second argument into a URL query string via URLSearchParams, so calling:

serverFetch.get<ApiResponseMyCoverletterListDTO>(
  "/coverletters/me",
  { memberId }
);

will result in a request to /coverletters/me?memberId=<number>. No changes are needed here.

api/resume.ts (1)

1-36: Clean and well-organized API client implementation!

The module maintains consistency with other API modules and provides a clear interface for resume-related operations. The separation of concerns between presigned URL generation and metadata saving is well-designed.

api/types/coverletter-types.ts (2)

1-11: Excellent type structure for cover letter creation.

The DTOs are well-structured with clear separation between creation and retrieval interfaces. The naming convention is consistent and follows good TypeScript practices.


13-18: Consistent API response wrapper pattern.

Good use of consistent API response wrapper interfaces across all endpoints. This provides standardized error handling and response structure throughout the application.

Also applies to: 33-38, 51-56

api/interview.ts (2)

1-11: Well-organized imports and type definitions.

Good use of explicit imports for all required types from the interview-types module. This ensures type safety across the API client.


13-19: Excellent implementation of typed API functions.

The functions are well-structured with proper generic typing, clear parameter definitions, and appropriate HTTP methods for each operation. The use of template literals for dynamic URLs is also well-implemented.

Also applies to: 21-30, 32-41, 43-52

components/cover-letter-form.tsx (4)

11-11: Good integration with member session context.

The use of useRequireMemberId hook properly ensures that only authenticated users can access the form functionality.

Also applies to: 29-29


14-25: Excellent refactoring of form data structure.

The change from a single title field to separate corporateName and jobName fields aligns well with the API requirements and provides better data granularity.


70-75: Comprehensive form validation logic.

The validation properly checks all required fields including both the new corporate/job name fields and ensures all Q&A pairs are complete.


80-99: Clear and user-friendly form fields.

The separate input fields for corporate name and job name provide better UX with appropriate placeholder text and labels.

api/types/resume-types.ts (3)

1-18: Well-structured resume creation types.

The DTOs properly separate creation data from result data, and the API response wrapper follows the established pattern.


20-31: Good support for presigned upload workflow.

The presigned upload DTOs properly support modern file upload patterns with S3 or similar storage services.


50-65: Consistent API response structure across all endpoints.

The resume list and detail response types maintain consistency with the established API response pattern, which will simplify error handling and data processing.

Comment on lines +14 to +20
if (diffHours < 24) {
return `${diffHours}시간 전`;
} else if (diffMinutes < 60) {
return `${diffMinutes}분 전`;
} else if (diffSeconds < 60) {
return `${diffSeconds}초 전`;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the conditional logic flow.

The current conditional logic has a fundamental flaw in the order of checks. The conditions should be structured to handle time units from largest to smallest, and the current logic allows fall-through to smaller units even when larger units should take precedence.

-  if (diffHours < 24) {
-    return `${diffHours}시간 전`;
-  } else if (diffMinutes < 60) {
-    return `${diffMinutes}분 전`;
-  } else if (diffSeconds < 60) {
-    return `${diffSeconds}초 전`;
-  }
+  if (diffHours >= 24) {
+    return `${year}년 ${month}월 ${day}일`;
+  } else if (diffHours > 0) {
+    return `${diffHours}시간 전`;
+  } else if (diffMinutes > 0) {
+    return `${diffMinutes}분 전`;
+  } else {
+    return `${diffSeconds}초 전`;
+  }
-
-  return `${year}년 ${month}월 ${day}일`;
📝 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
if (diffHours < 24) {
return `${diffHours}시간 전`;
} else if (diffMinutes < 60) {
return `${diffMinutes}분 전`;
} else if (diffSeconds < 60) {
return `${diffSeconds}초 전`;
}
if (diffHours >= 24) {
return `${year}${month}${day}일`;
} else if (diffHours > 0) {
return `${diffHours}시간 전`;
} else if (diffMinutes > 0) {
return `${diffMinutes}분 전`;
} else {
return `${diffSeconds}초 전`;
}
🤖 Prompt for AI Agents
In utils/date/convertDate.ts around lines 14 to 20, the conditional checks for
time differences are ordered incorrectly, causing smaller units like minutes or
seconds to be returned even when larger units like hours apply. Reorder the
conditions to check from largest to smallest time units, starting with hours,
then minutes, and finally seconds, ensuring the correct time unit is returned
based on the difference.

</div>

<Button variant="outline" className="w-full" onClick={() => (window.location.href = "/workspace")}>
<Button variant="outline" className="w-full" onClick={() => { login(1); window.location.href = "/workspace" }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Address hard-coded member ID and improve navigation.

The onClick handler has several issues that should be addressed:

  1. Hard-coded member ID: Using login(1) with a fixed ID appears to be temporary development code
  2. Navigation method: Using window.location.href bypasses Next.js routing and doesn't provide the benefits of client-side navigation
  3. Missing error handling: No error handling for the login operation

Consider this improved implementation:

-              <Button variant="outline" className="w-full" onClick={() => { login(1); window.location.href = "/workspace" }}>
+              <Button variant="outline" className="w-full" onClick={async () => {
+                try {
+                  // TODO: Replace with actual user ID from authentication
+                  await login(1);
+                  router.push("/workspace");
+                } catch (error) {
+                  console.error("Login failed:", error);
+                  // TODO: Show error message to user
+                }
+              }}>

You'll also need to import the router:

 "use client"
 import Link from "next/link"
+import { useRouter } from "next/navigation"
 import { Button } from "@/components/ui/button"

And add it to the component:

 export default function LoginPage() {
+  const router = useRouter()
   const { login } = useMemberSession()
🤖 Prompt for AI Agents
In app/login/page.tsx at line 44, replace the hard-coded login call login(1)
with a dynamic or state-driven member ID to avoid fixed values. Instead of using
window.location.href for navigation, use Next.js's useRouter hook and its
router.push method for client-side routing. Add proper error handling around the
login call to manage failures gracefully. Import useRouter from
'next/navigation', initialize it in the component, and update the onClick
handler to await login, handle errors, and navigate using router.push.

Comment on lines +4 to +6
export async function generateInterviewQuestions(payload: any) {
return aiFetch.post<any, any>("/ai/interview/questions", payload);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace any types with proper TypeScript interfaces.

Using any types defeats the purpose of TypeScript's type safety. Define proper interfaces for the request payload and response.

+interface InterviewQuestionPayload {
+  resumeId?: string
+  coverLetterId?: string
+  jobPosition?: string
+  interviewStyle?: 'personality' | 'technical'
+  questionCount?: number
+}
+
+interface InterviewQuestionResponse {
+  questions: Array<{
+    id: string
+    question: string
+    category?: string
+  }>
+}
+
-export async function generateInterviewQuestions(payload: any) {
-  return aiFetch.post<any, any>("/ai/interview/questions", payload);
+export async function generateInterviewQuestions(payload: InterviewQuestionPayload) {
+  return aiFetch.post<InterviewQuestionResponse, InterviewQuestionPayload>("/ai/interview/questions", payload);
 }
📝 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 async function generateInterviewQuestions(payload: any) {
return aiFetch.post<any, any>("/ai/interview/questions", payload);
}
interface InterviewQuestionPayload {
resumeId?: string
coverLetterId?: string
jobPosition?: string
interviewStyle?: 'personality' | 'technical'
questionCount?: number
}
interface InterviewQuestionResponse {
questions: Array<{
id: string
question: string
category?: string
}>
}
export async function generateInterviewQuestions(payload: InterviewQuestionPayload) {
return aiFetch.post<InterviewQuestionResponse, InterviewQuestionPayload>(
"/ai/interview/questions",
payload
);
}
🤖 Prompt for AI Agents
In api/ai-interview.ts around lines 4 to 6, the function
generateInterviewQuestions uses `any` types for both the payload parameter and
the response type, which undermines type safety. Define appropriate TypeScript
interfaces for the request payload structure and the expected response format,
then replace the `any` types with these interfaces in the function signature and
the aiFetch.post call to ensure proper type checking.

Comment on lines +38 to +44
// 4. 자기소개서 상세 조회
const coverletterDetail = await getCoverletterDetail(1);
console.log("coverletterDetail:", coverletterDetail);

// 5. 내 자기소개서 리스트 조회
const myCoverletters = await findMyCoverletter(1);
console.log("myCoverletters:", myCoverletters);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid hardcoded entity IDs in dependent API calls.

Using hardcoded coverletterId: 1 may cause test failures if this entity doesn't exist in the test environment.

Use the ID from the previously created cover letter:

// 4. 자기소개서 상세 조회
- const coverletterDetail = await getCoverletterDetail(1);
+ const coverletterDetail = await getCoverletterDetail(
+   coverletterResult.data?.coverletterId || 1
+ );
console.log("coverletterDetail:", coverletterDetail);

// 5. 내 자기소개서 리스트 조회
const myCoverletters = await findMyCoverletter(1);
📝 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
// 4. 자기소개서 상세 조회
const coverletterDetail = await getCoverletterDetail(1);
console.log("coverletterDetail:", coverletterDetail);
// 5. 내 자기소개서 리스트 조회
const myCoverletters = await findMyCoverletter(1);
console.log("myCoverletters:", myCoverletters);
// 4. 자기소개서 상세 조회
const coverletterDetail = await getCoverletterDetail(
coverletterResult.data?.coverletterId || 1
);
console.log("coverletterDetail:", coverletterDetail);
// 5. 내 자기소개서 리스트 조회
const myCoverletters = await findMyCoverletter(1);
console.log("myCoverletters:", myCoverletters);
🤖 Prompt for AI Agents
In api/api-test.ts around lines 38 to 44, avoid using the hardcoded
coverletterId value of 1 in the getCoverletterDetail and findMyCoverletter
calls. Instead, capture the ID from the cover letter created earlier in the test
and use that dynamic ID in these calls to ensure the tests do not fail due to
missing entities.

Comment on lines +54 to +57
// 내 인터뷰 리스트 조회
export async function getMyInterviewList() {
return serverFetch.get<any>("/interviews");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix missing type definition for interview list response.

The function uses any type which breaks type safety. Based on the pattern in other files, there should be a proper response type defined.

-export async function getMyInterviewList() {
-  return serverFetch.get<any>("/interviews");
-}
+export async function getMyInterviewList() {
+  return serverFetch.get<ApiResponseMyInterviewListDTO>("/interviews");
+}

You'll need to define the corresponding types in api/types/interview-types.ts:

export interface MyInterviewDTO {
  interviewId?: number;
  name?: string;
  corporateName?: string;
  jobName?: string;
  status?: string;
  createdAt?: string;
  // Add other relevant fields
}

export interface MyInterviewListDTO {
  interviews?: MyInterviewDTO[];
}

export interface ApiResponseMyInterviewListDTO {
  isSuccess?: boolean;
  code?: string;
  message?: string;
  result?: MyInterviewListDTO;
}
🤖 Prompt for AI Agents
In api/interview.ts around lines 54 to 57, the getMyInterviewList function
currently returns a response typed as any, which breaks type safety. To fix
this, define the appropriate response types in api/types/interview-types.ts as
suggested (MyInterviewDTO, MyInterviewListDTO, ApiResponseMyInterviewListDTO)
and update the function's return type to use ApiResponseMyInterviewListDTO
instead of any. This will ensure proper typing and consistency with other API
response patterns.

Comment on lines +63 to +68
export interface ApiResponseCreateMemberInterviewDTO {
isSuccess?: boolean;
code?: string;
message?: string;
result?: CreateMemberInterviewDTO;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

ApiResponseCreateMemberInterviewDTO returns the wrong result type

The API response should wrap CreateMemberInterviewResultDTO, not the request DTO. Returning the input shape loses server-generated fields such as memberInterviewId.

 export interface ApiResponseCreateMemberInterviewDTO {
   isSuccess?: boolean;
   code?: string;
   message?: string;
-  result?: CreateMemberInterviewDTO;
+  result?: CreateMemberInterviewResultDTO;
 }
📝 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 ApiResponseCreateMemberInterviewDTO {
isSuccess?: boolean;
code?: string;
message?: string;
result?: CreateMemberInterviewDTO;
}
export interface ApiResponseCreateMemberInterviewDTO {
isSuccess?: boolean;
code?: string;
message?: string;
result?: CreateMemberInterviewResultDTO;
}
🤖 Prompt for AI Agents
In api/types/interview-types.ts between lines 63 and 68, the
ApiResponseCreateMemberInterviewDTO interface incorrectly uses
CreateMemberInterviewDTO as the type for the result property. Update the result
property type to CreateMemberInterviewResultDTO to correctly represent the
server response including generated fields like memberInterviewId.

Comment on lines +57 to +63
export interface CreateMemberInterviewDTO {
memberId: number;
resumeId: number;
coverletterId: number;
}

export interface ApiResponseCreateMemberInterviewDTO {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inconsistent camel-casing – coverletterId vs coverLetterId used elsewhere

Most of the codebase (e.g. Step2.tsx, workspace/page.tsx) uses coverLetterId (capital “L”) while this DTO defines coverletterId (lower-case “l”). This small mismatch silently defeats type-safety and complicates auto-completion.

 export interface CreateMemberInterviewDTO {
   memberId: number;
   resumeId: number;
-  coverletterId: number;
+  coverLetterId: 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 CreateMemberInterviewDTO {
memberId: number;
resumeId: number;
coverletterId: number;
}
export interface ApiResponseCreateMemberInterviewDTO {
export interface CreateMemberInterviewDTO {
memberId: number;
resumeId: number;
coverLetterId: number;
}
export interface ApiResponseCreateMemberInterviewDTO {
🤖 Prompt for AI Agents
In api/types/interview-types.ts around lines 57 to 63, the property name
coverletterId in CreateMemberInterviewDTO uses inconsistent camel-casing
compared to coverLetterId used elsewhere. Rename coverletterId to coverLetterId
to match the rest of the codebase, ensuring consistent camel-casing for type
safety and better auto-completion.

Comment on lines +35 to +37
const queryClient = useQueryClient()
const memberId = typeof window !== 'undefined' ? Number(localStorage.getItem('memberId')) : undefined
// Dialog 상태
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

memberId extraction may yield 0 (truthy-falsey bug) and duplicates context logic

Number(localStorage.getItem('memberId')) returns 0 (falsy) when the key is absent, disabling the queries even though the value exists but is invalid. Moreover, the rest of the app already offers useRequireMemberId() for this purpose.

-const memberId = typeof window !== 'undefined' ? Number(localStorage.getItem('memberId')) : undefined
+const memberId = useRequireMemberId()          // reuse the centralised hook
+if (!memberId) throw new Error('memberId missing – user should be redirected by the hook')

This removes duplicated logic, prevents the accidental 0 case, and keeps the retrieval strategy consistent.

🤖 Prompt for AI Agents
In components/interview/steps/Step2.tsx around lines 35 to 37, replace the
manual extraction of memberId from localStorage using
Number(localStorage.getItem('memberId')) with the existing useRequireMemberId()
hook from the app. This avoids the falsy-zero bug and removes duplicated logic
by using the centralized memberId retrieval method, ensuring consistent and
correct memberId handling across the app.

Comment on lines +70 to 80
enabled: !!memberId,
select: (data) => {
const resumes = data.result?.resumes
return resumes?.map((resume) => ({
id: resume.resumeId,
name: resume.fileName,
date: null, // 상세조회에서 가져올 수 있음
size: resume.fileSize ? `${(resume.fileSize / 1024).toFixed(1)}KB` : '-',
type: "resume",
}))
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Resume items set date: null but later pass it to convertDate

convertDate(null) will likely yield "Invalid Date" or throw, leading to runtime issues.

-        date: null, // 상세조회에서 가져올 수 있음
+        date: resume.createdAt ?? '',   // preserve ISO string or empty for safety

Consider falling back to the detailed API only when the field is missing.

📝 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
enabled: !!memberId,
select: (data) => {
const resumes = data.result?.resumes
return resumes?.map((resume) => ({
id: resume.resumeId,
name: resume.fileName,
date: null, // 상세조회에서 가져올 수 있음
size: resume.fileSize ? `${(resume.fileSize / 1024).toFixed(1)}KB` : '-',
type: "resume",
}))
},
enabled: !!memberId,
select: (data) => {
const resumes = data.result?.resumes
return resumes?.map((resume) => ({
id: resume.resumeId,
name: resume.fileName,
- date: null, // 상세조회에서 가져올 수 있음
+ date: resume.createdAt ?? '', // preserve ISO string or empty for safety
size: resume.fileSize ? `${(resume.fileSize / 1024).toFixed(1)}KB` : '-',
type: "resume",
}))
},
🤖 Prompt for AI Agents
In app/workspace/page.tsx around lines 70 to 80, the resume items set the date
field to null, which causes runtime issues when passed to convertDate. Instead
of setting date to null, modify the code to omit the date or provide a valid
fallback value, and only call the detailed API to fetch the date if it is
missing. This prevents convertDate from receiving null and avoids invalid date
errors.

jpark0506 and others added 3 commits June 1, 2025 16:24
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@jpark0506 jpark0506 merged commit de26ec8 into main Jun 1, 2025
Copy link
Contributor

@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: 8

♻️ Duplicate comments (1)
app/workspace/page.tsx (1)

76-76: ⚠️ Potential issue

Critical issue: Passing null to convertDate function

This is a known issue from previous reviews. Setting date: null will cause runtime errors when convertDate(null) is called on line 236.

Apply this fix to handle the missing date properly:

-        date: null, // 상세조회에서 가져올 수 있음
+        date: resume.createdAt ?? '',
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea27bfe and 25e021c.

📒 Files selected for processing (1)
  • app/workspace/page.tsx (9 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
app/workspace/page.tsx

[error] 426-428: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (1)
app/workspace/page.tsx (1)

206-206: Good fix: Using unique ID for React keys

The change to use doc.id instead of doc.name for React keys addresses the previous review concern about potential key collisions.

{/* 기업 배지 */}
{doc.type === 'manual' && (
<Badge className="bg-blue-100 text-blue-800 whitespace-nowrap hover:bg-blue-100">
{(doc as any).corporateName}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve type safety for corporate name access

Using (doc as any).corporateName bypasses TypeScript's type checking and could lead to runtime errors.

Consider defining proper types for your document objects:

interface CoverLetterDocument {
  id: number;
  name: string;
  corporateName: string;
  jobName: string;
  date: string | null;
  size: string;
  type: 'manual';
}

interface ResumeDocument {
  id: number;
  name: string;
  date: string | null;
  size: string;
  type: 'resume';
}

type DocumentType = CoverLetterDocument | ResumeDocument;

Then use type guards instead of type assertions:

-                      {doc.type === 'manual' && (
-                        <Badge className="bg-blue-100 text-blue-800 whitespace-nowrap hover:bg-blue-100">
-                          {(doc as any).corporateName}
-                        </Badge>
-                      )}
+                      {doc.type === 'manual' && 'corporateName' in doc && (
+                        <Badge className="bg-blue-100 text-blue-800 whitespace-nowrap hover:bg-blue-100">
+                          {doc.corporateName}
+                        </Badge>
+                      )}
🤖 Prompt for AI Agents
In app/workspace/page.tsx at line 221, avoid using (doc as any).corporateName to
improve type safety. Define proper TypeScript interfaces for your document
objects, such as CoverLetterDocument and ResumeDocument, and create a union type
DocumentType. Then implement type guards to safely access corporateName only
when doc is of type CoverLetterDocument, replacing the any cast with proper type
checking.

try {
// 1. presigned url 발급
const presigned = await getPresignedUploadUrl(file.name)
const { presignedUrl, fileUrl } = presigned.result!
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix optional chaining issue

The static analysis tool correctly identifies that accessing presigned.result! properties could be safer with optional chaining.

Apply this fix to improve null safety:

-      const { presignedUrl, fileUrl } = presigned.result!
+      const { presignedUrl, fileUrl } = presigned.result ?? {}

Also add validation to ensure the URLs exist before proceeding:

+      if (!presignedUrl || !fileUrl) {
+        throw new Error('Presigned URL generation failed')
+      }
📝 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 { presignedUrl, fileUrl } = presigned.result!
// replace non-null assertion with safe fallback
- const { presignedUrl, fileUrl } = presigned.result!
+ const { presignedUrl, fileUrl } = presigned.result ?? {}
// ensure URLs are present before proceeding
+ if (!presignedUrl || !fileUrl) {
+ throw new Error('Presigned URL generation failed')
+ }
🤖 Prompt for AI Agents
In app/workspace/page.tsx at line 418, replace the non-null assertion on
presigned.result with optional chaining to safely access presignedUrl and
fileUrl. Additionally, add validation checks to confirm that both URLs exist
before using them, preventing runtime errors from undefined values.

// 2. S3 업로드
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('PUT', presignedUrl!, true)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential null pointer access

The presignedUrl could be undefined, causing a runtime error.

Apply this fix:

-        xhr.open('PUT', presignedUrl!, true)
+        xhr.open('PUT', presignedUrl, true)

This change works together with the validation added in the previous suggestion.

📝 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
xhr.open('PUT', presignedUrl!, true)
xhr.open('PUT', presignedUrl, true)
🤖 Prompt for AI Agents
In app/workspace/page.tsx at line 422, the code uses presignedUrl without
checking if it is undefined, which can cause a runtime error. Add a conditional
check before this line to ensure presignedUrl is defined; if it is undefined,
handle the error appropriately or return early to prevent calling xhr.open with
an invalid URL.


return (
<div className="space-y-4">
<Input type="file" accept="application/pdf" onChange={handleFileChange} disabled={isUploading} />
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add file validation for security

The file input should validate file types and sizes to prevent security issues and improve user experience.

Add file validation:

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files[0]) {
-      setFile(e.target.files[0])
-      setError(null)
+      const selectedFile = e.target.files[0]
+      
+      // Validate file type
+      if (selectedFile.type !== 'application/pdf') {
+        setError('PDF 파일만 업로드 가능합니다.')
+        return
+      }
+      
+      // Validate file size (e.g., 10MB limit)
+      if (selectedFile.size > 10 * 1024 * 1024) {
+        setError('파일 크기는 10MB 이하여야 합니다.')
+        return
+      }
+      
+      setFile(selectedFile)
+      setError(null)
    }
  }
📝 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
<Input type="file" accept="application/pdf" onChange={handleFileChange} disabled={isUploading} />
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const selectedFile = e.target.files[0]
// Validate file type
if (selectedFile.type !== 'application/pdf') {
setError('PDF 파일만 업로드 가능합니다.')
return
}
// Validate file size (e.g., 10MB limit)
if (selectedFile.size > 10 * 1024 * 1024) {
setError('파일 크기는 10MB 이하여야 합니다.')
return
}
setFile(selectedFile)
setError(null)
}
}
🤖 Prompt for AI Agents
In app/workspace/page.tsx at line 456, the file input lacks validation for file
type and size, which can lead to security risks and poor user experience.
Enhance the handleFileChange function to check that the selected file is a PDF
and its size is within acceptable limits before proceeding. If the file does not
meet these criteria, prevent further processing and provide appropriate user
feedback.

<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredDocuments.length > 0 ? (
filteredDocuments.map((doc) => (
{filteredDocuments && filteredDocuments?.length > 0 ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add loading states for better UX

The component should show loading indicators while data is being fetched.

Add loading state handling:

+          {(coverLetterListLoading || resumeListLoading) && (
+            <div className="col-span-full py-12 text-center">
+              <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#8FD694] mx-auto"></div>
+              <p className="mt-2 text-gray-500">문서를 불러오는 중...</p>
+            </div>
+          )}
           {filteredDocuments && filteredDocuments?.length > 0 ? (
📝 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
{filteredDocuments && filteredDocuments?.length > 0 ? (
{(coverLetterListLoading || resumeListLoading) && (
<div className="col-span-full py-12 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#8FD694] mx-auto"></div>
<p className="mt-2 text-gray-500">문서를 불러오는 중...</p>
</div>
)}
{filteredDocuments && filteredDocuments?.length > 0 ? (
🤖 Prompt for AI Agents
In app/workspace/page.tsx at line 204, the component currently does not handle
loading states, which can lead to poor user experience during data fetching. Add
a loading state variable to track when data is being fetched and render a
loading indicator or message when this state is true. Ensure the loading
indicator is displayed before filteredDocuments are available, and only show the
documents list once loading is complete.

setProgress(100)
onSuccess()
alert('이력서가 성공적으로 업로드되었습니다.')
} catch (e: any) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling in upload function

The error handling could be more specific and user-friendly.

Enhance error handling:

    } catch (e: any) {
-      setError(e.message || '업로드 실패')
-      alert('업로드에 실패했습니다. 다시 시도해주세요.')
+      const errorMessage = e.message || '업로드 실패'
+      setError(errorMessage)
+      
+      // More specific error messages
+      if (errorMessage.includes('S3')) {
+        alert('파일 업로드에 실패했습니다. 네트워크 연결을 확인해주세요.')
+      } else if (errorMessage.includes('Presigned')) {
+        alert('업로드 준비에 실패했습니다. 잠시 후 다시 시도해주세요.')
+      } else {
+        alert('업로드에 실패했습니다. 다시 시도해주세요.')
+      }
    } finally {
📝 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
} catch (e: any) {
} catch (e: any) {
const errorMessage = e.message || '업로드 실패'
setError(errorMessage)
// More specific error messages
if (errorMessage.includes('S3')) {
alert('파일 업로드에 실패했습니다. 네트워크 연결을 확인해주세요.')
} else if (errorMessage.includes('Presigned')) {
alert('업로드 준비에 실패했습니다. 잠시 후 다시 시도해주세요.')
} else {
alert('업로드에 실패했습니다. 다시 시도해주세요.')
}
} finally {
🤖 Prompt for AI Agents
In app/workspace/page.tsx at line 446, the catch block currently catches errors
as a generic any type, which limits specific error handling and user-friendly
messages. Refine the error handling by narrowing the error type if possible, and
provide more descriptive, user-friendly error messages or handling logic based
on the error type or properties. This may include checking error codes or
messages and responding accordingly to improve clarity and user experience.

Comment on lines +67 to +81
const { data: resumeList, isLoading: resumeListLoading } = useQuery({
queryKey: ["resumeList", memberId],
queryFn: () => getResumeList(memberId!),
enabled: !!memberId,
select: (data) => {
const resumes = data.result?.resumes
return resumes?.map((resume) => ({
id: resume.resumeId,
name: resume.fileName,
date: null, // 상세조회에서 가져올 수 있음
size: resume.fileSize ? `${(resume.fileSize / 1024).toFixed(1)}KB` : '-',
type: "resume",
}))
},
{
id: 2,
name: "네이버 지원 자소서.docx",
date: "2023-04-18",
size: "450KB",
type: "file",
category: "coverLetter",
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Apply same error handling pattern to resume list

The resume list query also needs proper error handling for consistency.

Add similar error handling:

  const { data: resumeList, isLoading: resumeListLoading, error: resumeError } = useQuery({
    queryKey: ["resumeList", memberId],
    queryFn: () => getResumeList(memberId!),
    enabled: !!memberId,
+    retry: 2,
+    staleTime: 5 * 60 * 1000,
    select: (data) => {
      const resumes = data.result?.resumes
      return resumes?.map((resume) => ({
        id: resume.resumeId,
        name: resume.fileName,
        date: resume.createdAt ?? '',
        size: resume.fileSize ? `${(resume.fileSize / 1024).toFixed(1)}KB` : '-',
        type: "resume",
      }))
    },
  })

+ if (resumeError) {
+   console.error('Failed to load resumes:', resumeError)
+ }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/workspace/page.tsx around lines 67 to 81, the useQuery call for fetching
the resume list lacks error handling. Add an onError callback to the useQuery
options that handles errors consistently, such as logging the error or showing a
user-friendly message, following the same pattern used in other queries in this
file.

Comment on lines +48 to +64
const { data: coverLetterList, isLoading: coverLetterListLoading } = useQuery({
queryKey: ["coverLetterList", memberId],
queryFn: () => findMyCoverletter(memberId!),
enabled: !!memberId,
select: (data) => {
const coverLetters = data.result?.coverletters;
return coverLetters?.map((coverLetter) => ({
id: coverLetter.coverletterId,
name: `${coverLetter.corporateName}-${coverLetter.jobName}`,
corporateName: coverLetter.corporateName,
jobName: coverLetter.jobName,
date: coverLetter.createdAt,
size: "0KB",
type: "manual",
}))
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider error handling for API failures

The cover letter list query doesn't handle API errors, which could lead to poor user experience.

Add error handling to the query:

  const { data: coverLetterList, isLoading: coverLetterListLoading, error: coverLetterError } = useQuery({
    queryKey: ["coverLetterList", memberId],
    queryFn: () => findMyCoverletter(memberId!),
    enabled: !!memberId,
+    retry: 2,
+    staleTime: 5 * 60 * 1000, // 5 minutes
    select: (data) => {
      const coverLetters = data.result?.coverletters;
      return coverLetters?.map((coverLetter) => ({
        id: coverLetter.coverletterId,
        name: `${coverLetter.corporateName}-${coverLetter.jobName}`,
        corporateName: coverLetter.corporateName,
        jobName: coverLetter.jobName,
        date: coverLetter.createdAt,
        size: "0KB",
        type: "manual",
      }))
    }
  })

+ if (coverLetterError) {
+   console.error('Failed to load cover letters:', coverLetterError)
+ }
📝 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 { data: coverLetterList, isLoading: coverLetterListLoading } = useQuery({
queryKey: ["coverLetterList", memberId],
queryFn: () => findMyCoverletter(memberId!),
enabled: !!memberId,
select: (data) => {
const coverLetters = data.result?.coverletters;
return coverLetters?.map((coverLetter) => ({
id: coverLetter.coverletterId,
name: `${coverLetter.corporateName}-${coverLetter.jobName}`,
corporateName: coverLetter.corporateName,
jobName: coverLetter.jobName,
date: coverLetter.createdAt,
size: "0KB",
type: "manual",
}))
}
})
const { data: coverLetterList, isLoading: coverLetterListLoading, error: coverLetterError } = useQuery({
queryKey: ["coverLetterList", memberId],
queryFn: () => findMyCoverletter(memberId!),
enabled: !!memberId,
retry: 2,
staleTime: 5 * 60 * 1000, // 5 minutes
select: (data) => {
const coverLetters = data.result?.coverletters;
return coverLetters?.map((coverLetter) => ({
id: coverLetter.coverletterId,
name: `${coverLetter.corporateName}-${coverLetter.jobName}`,
corporateName: coverLetter.corporateName,
jobName: coverLetter.jobName,
date: coverLetter.createdAt,
size: "0KB",
type: "manual",
}));
}
});
if (coverLetterError) {
console.error('Failed to load cover letters:', coverLetterError);
}
🤖 Prompt for AI Agents
In app/workspace/page.tsx around lines 48 to 64, the useQuery call for fetching
the cover letter list lacks error handling, which can cause issues if the API
call fails. Add an onError callback to the useQuery options to handle API errors
gracefully, such as logging the error or showing a user-friendly message. This
will improve the user experience by managing failures properly.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants