[JDDEV-89] Feature: home page refactoring#105
Conversation
📝 WalkthroughWalkthroughChangesMock apply flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Editor
participant useDebounce
participant API
participant Header
Editor->>useDebounce: debounce edited payload
useDebounce-->>Editor: return stable payload
Editor->>API: save or update payload
API-->>Editor: return successful save
Editor->>Header: display last saved time
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@jobdri/src/app/mockApply/job/`[jobPostingId]/review/page.tsx:
- Around line 345-347: Update the new-posting branch in handleNext to call
setJobPostingId with savedJobPosting.jobPostingId immediately after
saveJobPosting succeeds, before createApplyFromJobPosting runs, while leaving
the existing update path unchanged.
- Around line 178-214: Remove jobPostingId from the autosave useEffect
dependency trigger and maintain its current value through a ref updated when the
ID changes. Have autoSave read the ref when deciding between saveJobPosting and
updateJobPosting, while preserving setJobPostingId after the initial save so
creating a posting cannot immediately trigger a duplicate update.
In `@jobdri/src/app/page.tsx`:
- Around line 37-47: Update page.tsx’s dashboard loading flow to aggregate every
page from fetchMyMockApplies and fetchMyJobPostings before deriving
inProgressList, completedList, and jobPostings, using totalPages/content
metadata and the existing page/size API parameters. Update fetchMyJobPostings in
jobPostings.ts and fetchMyMockApplies in mockApplies.ts as needed to expose or
support pagination, preserving current unauthorized handling while concatenating
all page contents.
In `@jobdri/src/utils/date.ts`:
- Around line 23-24: Update the date-difference calculation in the surrounding
date utility to compare calendar dates using Date.UTC values for today and
targetDay, rather than flooring elapsed local-millisecond differences. Preserve
the existing day-label behavior while ensuring consecutive calendar dates remain
one day apart across DST transitions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 749fb879-eeec-4132-af5b-ee0ee4e4bb89
📒 Files selected for processing (9)
jobdri/src/app/mockApply/[mockApplyId]/page.tsxjobdri/src/app/mockApply/job/[jobPostingId]/review/page.tsxjobdri/src/app/page.tsxjobdri/src/components/common/lnb/Lnb.tsxjobdri/src/components/mockApply/home/applicationHomeUtils.tsjobdri/src/hooks/useDebounce.tsjobdri/src/lib/api/jobPostings.tsjobdri/src/lib/api/mockApplies.tsjobdri/src/utils/date.ts
| useEffect(() => { | ||
| if ( | ||
| isLoading || | ||
| debouncedPayload.detailClassificationId <= 0 || | ||
| !debouncedPayload.postingName || | ||
| !debouncedPayload.companyName || | ||
| !debouncedPayload.jobTitle | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| if (isInitialRender.current) { | ||
| isInitialRender.current = false; | ||
| return; | ||
| } | ||
|
|
||
| const autoSave = async () => { | ||
| try { | ||
| let savedResult; | ||
| if (jobPostingId) { | ||
| savedResult = await updateJobPosting(jobPostingId, debouncedPayload); | ||
| } else { | ||
| savedResult = await saveJobPosting(debouncedPayload); | ||
| setJobPostingId(savedResult.jobPostingId); | ||
| } | ||
|
|
||
| const now = new Date(); | ||
| setLastSavedTime( | ||
| `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`, | ||
| ); | ||
| } catch (error) { | ||
| console.error("채용 공고 자동 저장 실패:", error); | ||
| } | ||
| }; | ||
|
|
||
| autoSave(); | ||
| }, [debouncedPayload, jobPostingId, isLoading]); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
jobPostingId in the effect's dependency list causes a guaranteed duplicate save right after creating a new posting.
When a new posting is created inside autoSave (line 200), setJobPostingId fires and — because jobPostingId is a dependency of this same effect — re-triggers it with the unchanged debouncedPayload. Since isInitialRender.current is already false, this immediately issues a second, redundant updateJobPosting call with identical data right after the saveJobPosting POST. This happens on every first-time autosave, not just an edge case.
🔧 Proposed fix — read `jobPostingId` via a ref instead of a dependency
+ const jobPostingIdRef = useRef(jobPostingId);
+ useEffect(() => {
+ jobPostingIdRef.current = jobPostingId;
+ }, [jobPostingId]);
+
useEffect(() => {
if (
isLoading ||
debouncedPayload.detailClassificationId <= 0 ||
!debouncedPayload.postingName ||
!debouncedPayload.companyName ||
!debouncedPayload.jobTitle
) {
return;
}
if (isInitialRender.current) {
isInitialRender.current = false;
return;
}
const autoSave = async () => {
try {
let savedResult;
- if (jobPostingId) {
- savedResult = await updateJobPosting(jobPostingId, debouncedPayload);
+ if (jobPostingIdRef.current) {
+ savedResult = await updateJobPosting(
+ jobPostingIdRef.current,
+ debouncedPayload,
+ );
} else {
savedResult = await saveJobPosting(debouncedPayload);
setJobPostingId(savedResult.jobPostingId);
}
...
} catch (error) {
console.error("채용 공고 자동 저장 실패:", error);
}
};
autoSave();
- }, [debouncedPayload, jobPostingId, isLoading]);
+ }, [debouncedPayload, isLoading]);📝 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.
| useEffect(() => { | |
| if ( | |
| isLoading || | |
| debouncedPayload.detailClassificationId <= 0 || | |
| !debouncedPayload.postingName || | |
| !debouncedPayload.companyName || | |
| !debouncedPayload.jobTitle | |
| ) { | |
| return; | |
| } | |
| if (isInitialRender.current) { | |
| isInitialRender.current = false; | |
| return; | |
| } | |
| const autoSave = async () => { | |
| try { | |
| let savedResult; | |
| if (jobPostingId) { | |
| savedResult = await updateJobPosting(jobPostingId, debouncedPayload); | |
| } else { | |
| savedResult = await saveJobPosting(debouncedPayload); | |
| setJobPostingId(savedResult.jobPostingId); | |
| } | |
| const now = new Date(); | |
| setLastSavedTime( | |
| `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`, | |
| ); | |
| } catch (error) { | |
| console.error("채용 공고 자동 저장 실패:", error); | |
| } | |
| }; | |
| autoSave(); | |
| }, [debouncedPayload, jobPostingId, isLoading]); | |
| const jobPostingIdRef = useRef(jobPostingId); | |
| useEffect(() => { | |
| jobPostingIdRef.current = jobPostingId; | |
| }, [jobPostingId]); | |
| useEffect(() => { | |
| if ( | |
| isLoading || | |
| debouncedPayload.detailClassificationId <= 0 || | |
| !debouncedPayload.postingName || | |
| !debouncedPayload.companyName || | |
| !debouncedPayload.jobTitle | |
| ) { | |
| return; | |
| } | |
| if (isInitialRender.current) { | |
| isInitialRender.current = false; | |
| return; | |
| } | |
| const autoSave = async () => { | |
| try { | |
| let savedResult; | |
| if (jobPostingIdRef.current) { | |
| savedResult = await updateJobPosting( | |
| jobPostingIdRef.current, | |
| debouncedPayload, | |
| ); | |
| } else { | |
| savedResult = await saveJobPosting(debouncedPayload); | |
| setJobPostingId(savedResult.jobPostingId); | |
| } | |
| const now = new Date(); | |
| setLastSavedTime( | |
| `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`, | |
| ); | |
| } catch (error) { | |
| console.error("채용 공고 자동 저장 실패:", error); | |
| } | |
| }; | |
| autoSave(); | |
| }, [debouncedPayload, isLoading]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/app/mockApply/job/`[jobPostingId]/review/page.tsx around lines 178
- 214, Remove jobPostingId from the autosave useEffect dependency trigger and
maintain its current value through a ref updated when the ID changes. Have
autoSave read the ref when deciding between saveJobPosting and updateJobPosting,
while preserving setJobPostingId after the initial save so creating a posting
cannot immediately trigger a duplicate update.
| const savedJobPosting = jobPostingId | ||
| ? await updateJobPosting(jobPostingId, payload) | ||
| : await saveJobPosting(payload); | ||
| ? await updateJobPosting(jobPostingId, currentPayload) | ||
| : await saveJobPosting(currentPayload); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
Missing setJobPostingId after creating a new posting in handleNext risks duplicate job-posting creation.
Unlike the autosave branch (line 201), this "create new" path never calls setJobPostingId(savedJobPosting.jobPostingId). If createApplyFromJobPosting (line 349) throws after the posting was successfully created, the catch block (lines 358-365) leaves jobPostingId as null and only shows saveErrorMessage. Any retry of "다음으로" — or a subsequent autosave firing — will call saveJobPosting again, creating a second orphaned job-posting record for the same edit session. saveJobPosting is a non-idempotent POST with no safeguard against this repeat call.
🔧 Proposed fix
const savedJobPosting = jobPostingId
? await updateJobPosting(jobPostingId, currentPayload)
: await saveJobPosting(currentPayload);
+ if (!jobPostingId) {
+ setJobPostingId(savedJobPosting.jobPostingId);
+ }
+
const createdApply = await createApplyFromJobPosting({
jobPostingId: savedJobPosting.jobPostingId,
applyType: getSelectedApplyType(),
});📝 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.
| const savedJobPosting = jobPostingId | |
| ? await updateJobPosting(jobPostingId, payload) | |
| : await saveJobPosting(payload); | |
| ? await updateJobPosting(jobPostingId, currentPayload) | |
| : await saveJobPosting(currentPayload); | |
| const savedJobPosting = jobPostingId | |
| ? await updateJobPosting(jobPostingId, currentPayload) | |
| : await saveJobPosting(currentPayload); | |
| if (!jobPostingId) { | |
| setJobPostingId(savedJobPosting.jobPostingId); | |
| } | |
| const createdApply = await createApplyFromJobPosting({ | |
| jobPostingId: savedJobPosting.jobPostingId, | |
| applyType: getSelectedApplyType(), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/app/mockApply/job/`[jobPostingId]/review/page.tsx around lines 345
- 347, Update the new-posting branch in handleNext to call setJobPostingId with
savedJobPosting.jobPostingId immediately after saveJobPosting succeeds, before
createApplyFromJobPosting runs, while leaving the existing update path
unchanged.
| const [data, fetchedJobPostings] = await Promise.all([ | ||
| fetchMyMockApplies({ redirectOnUnauthorized: false }), | ||
| fetchMyJobPostings({ redirectOnUnauthorized: false }).catch(() => []), | ||
| ]); | ||
|
|
||
| const jobPostings = Array.isArray(fetchedJobPostings) | ||
| ? fetchedJobPostings | ||
| : []; | ||
|
|
||
| const inProgressList = data?.inProgress || []; | ||
| const completedList = data?.completed?.content || []; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)jobdri/src/(app/page\.tsx|lib/api/(jobPostings|mockApplies)\.ts)$' || true
echo "== page outline =="
ast-grep outline jobdri/src/app/page.tsx --view compact || true
echo "== relevant page lines =="
sed -n '1,90p' jobdri/src/app/page.tsx
echo "== jobPostings relevant lines =="
sed -n '1,220p' jobdri/src/lib/api/jobPostings.ts
sed -n '220,420p' jobdri/src/lib/api/jobPostings.ts
echo "== mockApplies relevant lines =="
sed -n '1,180p' jobdri/src/lib/api/mockApplies.ts
echo "== API pagination references =="
rg -n "PaginatedResponse|content|page|size|size=|page=" jobdri/src -S || trueRepository: JobDri-Developer/FrontEnd
Length of output: 49837
🌐 Web query:
Spring Framework RestControllerAdvice default exception handling pagination size parameter name content totalPages totalElements Page
💡 Result:
In the Spring Framework, @RestControllerAdvice is a specialized @ControllerAdvice that automatically applies @ResponseBody to all @ExceptionHandler methods, making it ideal for global REST exception handling [1][2]. By default, it applies globally to all controllers unless specific scoping attributes (like basePackages or annotations) are used [1][2]. When serializing Spring Data Page objects (such as PageImpl) to JSON, Spring defaults to a specific structure containing pagination metadata [3][4][5]. The standard fields included in the page metadata section are [3][5]: - size: The number of items per page [3][6]. - totalElements: The total number of items available across all pages [3][6]. - totalPages: The total number of pages [3][6]. - number: The current page number (0-indexed) [3][6]. Note that the default JSON envelope structure for pagination is not formally specified and is subject to change [5]. If you need to customize these field names or the structure for your API, Spring provides several approaches: 1. DTO Wrapper: Create a custom DTO class to wrap the Page object and use @JsonProperty annotations to rename the fields as desired [7][8]. 2. Custom Serializer: Implement a custom Jackson JsonSerializer for the Page implementation and register it as a @JsonComponent [8]. 3. PagedModel: Use Spring Data's PagedModel, which is designed to provide stable JSON representations of a Page [9][5]. For global exception handling, extending ResponseEntityExceptionHandler is recommended, as it provides built-in handling for common Spring MVC exceptions using the RFC 9457 Problem Detail format [10][11]. If you need to return custom pagination information within an error response, you can extend ProblemDetail to include your specific pagination metadata fields [10].
Citations:
- 1: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RestControllerAdvice.html
- 2: Clarify what @RestControllerAdvice vs @ControllerAdvice apply to by default spring-projects/spring-framework#34866
- 3: https://docs.spring.io/spring-data/rest/reference/paging-and-sorting.html
- 4: https://docs.spring.io/spring-data/rest/reference/data-commons/api/java/org/springframework/data/domain/PageImpl.html
- 5: https://docs.spring.io/spring-data/rest/reference/data-commons/repositories/core-extensions.html
- 6: https://docs.spring.io/spring-data/commons/reference/api/java/org/springframework/data/web/PagedModel.PageMetadata.html
- 7: https://stackoverflow.com/questions/36535644/is-it-possible-to-change-the-json-names-for-spring-data-pageable-responses
- 8: https://stackoverflow.com/questions/63087074/pagingandsortingrepository-custom-pageable-response-structure
- 9: https://docs.spring.io/spring-data/commons/reference/api/java/org/springframework/data/web/PagedModel.html
- 10: https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html
- 11: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.html
Load every paged result before rendering the dashboard.
Both Home data fetchers return page metadata (totalPages, content, size), but the API calls do not pass page/size and Home only maps the first returned content list. Users with more than one page of saved postings or completed applications will have stale/partial dashboard state. Collect totalPages and concatenate subsequent pages, or expose pagination arguments/API helpers that aggregate all pages before building lists.
📍 Affects 3 files
jobdri/src/app/page.tsx#L37-L47(this comment)jobdri/src/lib/api/jobPostings.ts#L364-L381jobdri/src/lib/api/mockApplies.ts#L132-L140
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/app/page.tsx` around lines 37 - 47, Update page.tsx’s dashboard
loading flow to aggregate every page from fetchMyMockApplies and
fetchMyJobPostings before deriving inProgressList, completedList, and
jobPostings, using totalPages/content metadata and the existing page/size API
parameters. Update fetchMyJobPostings in jobPostings.ts and fetchMyMockApplies
in mockApplies.ts as needed to expose or support pagination, preserving current
unauthorized handling while concatenating all page contents.
| const diffTime = today.getTime() - targetDay.getTime(); | ||
| const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle DST calendar-day boundaries.
Across a spring DST transition, consecutive local midnights can be 23 hours apart, so yesterday is rendered as 오늘. Compare calendar dates via Date.UTC(...) rather than flooring elapsed milliseconds.
Proposed fix
- const diffTime = today.getTime() - targetDay.getTime();
- const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
+ const diffDays =
+ (Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()) -
+ Date.UTC(
+ targetDay.getFullYear(),
+ targetDay.getMonth(),
+ targetDay.getDate(),
+ )) /
+ (1000 * 60 * 60 * 24);📝 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.
| const diffTime = today.getTime() - targetDay.getTime(); | |
| const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); | |
| const diffDays = | |
| (Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()) - | |
| Date.UTC( | |
| targetDay.getFullYear(), | |
| targetDay.getMonth(), | |
| targetDay.getDate(), | |
| )) / | |
| (1000 * 60 * 60 * 24); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/utils/date.ts` around lines 23 - 24, Update the date-difference
calculation in the surrounding date utility to compare calendar dates using
Date.UTC values for today and targetDay, rather than flooring elapsed
local-millisecond differences. Preserve the existing day-label behavior while
ensuring consecutive calendar dates remain one day apart across DST transitions.
🔗 관련 이슈
JDDEV-89
📝 개요
⌨️ 작업 상세 내용
1. 내 모의지원 홈 (
Home.tsx) 버그 수정 및 리팩토링createdAt데이터를 활용하여 날짜가 정상적으로 표시되도록 수정했습니다.Iterable런타임 에러 방어 로직 추가completedList) 데이터를 스프레드 연산자(...)로 병합할 때 발생하던is not iterable에러를 수정했습니다. (?.content ?? []등 방어 코드 적용)as) 및 매핑 코드를 유틸 함수(mapMockApplyToApplication)를 활용하는 방식으로 리팩토링하여 코드 가독성과 타입 안정성을 높였습니다.undefined가 될 수 있는 속성들에 대해 빈 문자열(|| "") 폴백(Fallback) 처리를 추가하여 타입 에러를 해결했습니다.2. API 타입 정의 최신화 (
mockApplies.ts)completed)의 응답 형태가 배열([])이 아닌 페이지네이션 객체임을 반영하여PageResponse<T>제네릭 인터페이스를 추가하고 타입을 알맞게 수정했습니다.MockApplyHomeList)을 정리하고fetchMyMockApplies함수의 리턴 로직을 스펙에 맞게 수정했습니다.3. LNB 컴포넌트 (
Lnb.tsx) 버그 수정Lnb.tsx내recentItems의 상태 초기값이 빈 배열([])로 설정되어 있어 최근 기록이 보이지 않던 문제를 수정했습니다. (미리 정의된defaultRecentItems를 초기값으로 연결)4. 공통 유틸리티 추가 (
utils/date.ts)formatRelativeDate) 구현오늘,어제,N일전으로 표시되며, 7일이 초과된 데이터는YY.MM.DD포맷으로 반환되도록 시간(Time) 값 제외 순수 일(Day) 단위 비교 로직을 적용했습니다.💡 코드 설명 및 참고사항
📸 스크린샷 (UI 변경 시)
🔍 리뷰 요구사항 (Reviewers)
Summary by CodeRabbit
New Features
Bug Fixes