Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 Walkthrough개요고객 및 사업자 앱에서 React Query 기반 인증 플로우를 구현하고, 로그인/가입 페이지를 컴포넌트로 분해하며, API 토큰 관리 시스템을 추가하고, 등록 폼 상태를 제어 컴포넌트로 리팩터링했습니다. Zustand 스토어를 통한 상태 관리 및 여러 뮤테이션/쿼리 훅을 도입했습니다. 변경 사항
시퀀스 다이어그램sequenceDiagram
actor User
participant LoginPage as 로그인 페이지
participant LoginForm as LoginForm<br/>컴포넌트
participant useLoginMutation as useLoginMutation<br/>훅
participant API as API 클라이언트
participant Server as 서버
participant Storage as localStorage
User->>LoginPage: 이메일/비밀번호 입력
LoginPage->>LoginForm: 값 변경 감지
LoginForm->>LoginForm: 상태 업데이트
User->>LoginForm: 로그인 버튼 클릭
LoginForm->>useLoginMutation: handleLogin 호출
useLoginMutation->>API: loginMutation.mutate()
API->>Server: POST /login (email, password)
Server-->>API: { accessToken, ... }
API-->>useLoginMutation: 성공 응답
useLoginMutation->>Storage: accessToken 저장
useLoginMutation->>LoginPage: /main으로 이동
sequenceDiagram
actor User
participant SignupPage as 가입 페이지
participant SignupForm as 폼 컴포넌트
participant useSignupMutation as useSignupMutation<br/>훅
participant API as API 클라이언트
participant Server as 서버
User->>SignupPage: 회원 정보 입력
SignupPage->>SignupForm: value/onChange props 전달
SignupForm->>SignupForm: 폼 상태 업데이트
User->>SignupPage: 가입 버튼 클릭
SignupPage->>SignupPage: 유효성 검사
alt 유효성 검사 통과
SignupPage->>useSignupMutation: mutate({ memberName, ... })
useSignupMutation->>API: signup 호출
API->>Server: POST /signup
Server-->>API: 성공/실패 응답
API-->>useSignupMutation: 결과 반환
useSignupMutation->>SignupPage: 다음 단계로 이동 또는 오류 표시
else 유효성 검사 실패
SignupPage->>SignupPage: 오류 메시지 표시
end
예상 코드 리뷰 소요 시간🎯 4 (복잡) | ⏱️ ~60분 관련 PR
제안 라벨
시
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx (1)
46-68:⚠️ Potential issue | 🟠 Major중첩
<button>구조를 제거해주세요.현재 삭제 버튼이 업로드 버튼 내부에 있어 HTML 사양 위반입니다. 이는 접근성 문제를 야기하고 키보드/포커스 관리가 불안정해질 수 있습니다.
event.stopPropagation()처리로 일부 동작하지만 구조적 문제는 해결되지 않습니다.🔧 제안 수정안
- <button - type="button" - onClick={handleOpenFilePicker} - className="relative flex h-[18rem] w-full items-center justify-center overflow-hidden rounded-[10px] bg-background" - > - {previewUrl ? ( - <> + <div className="relative"> + <button + type="button" + onClick={handleOpenFilePicker} + className="flex h-[18rem] w-full items-center justify-center overflow-hidden rounded-[10px] bg-background" + > + {previewUrl ? ( <Image src={previewUrl} alt="업로드한 대표 이미지 미리보기" fill className="object-cover" /> - - <button - type="button" - onClick={handleClickRemove} - className="absolute right-[0.8rem] top-[0.8rem] z-10 flex h-[3.2rem] w-[3.2rem] items-center justify-center rounded-full bg-white/90 shadow" - aria-label="사진 삭제" - > - <Icon name="CloseButton" width={16} height={16} /> - </button> - </> - ) : ( - <Icon name="Camera" width={24} height={24} /> - )} - </button> + ) : ( + <Icon name="Camera" width={24} height={24} /> + )} + </button> + {previewUrl && ( + <button + type="button" + onClick={handleClickRemove} + className="absolute right-[0.8rem] top-[0.8rem] z-10 flex h-[3.2rem] w-[3.2rem] items-center justify-center rounded-full bg-white/90 shadow" + aria-label="사진 삭제" + > + <Icon name="CloseButton" width={16} height={16} /> + </button> + )} + </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx` around lines 46 - 68, The delete <button> is nested inside the upload <button> (the element using handleOpenFilePicker/previewUrl/Image), which violates HTML specs; refactor so the delete control is not a child of the upload button: move the delete control (currently rendering Icon and using handleClickRemove) out to be a sibling element (e.g., a positioned <button> or <div role="button">) adjacent to the upload <button>, preserve its styling and aria-label="사진 삭제", and keep any event handlers (handleClickRemove) but remove reliance on event.stopPropagation() to prevent nested-button issues; ensure keyboard focus and onKey handlers remain functional for the new control.
🧹 Nitpick comments (4)
apps/customer/src/app/signup/page.tsx (1)
38-39: 폼 submit 이벤트로 전환하면 접근성과 입력 UX가 개선됩니다현재는 버튼 클릭 기반이라 입력 중 Enter 제출 흐름이 약합니다.
<form onSubmit>+type="submit"으로 바꾸면 키보드/모바일 키패드 UX가 좋아집니다.Also applies to: 131-137
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer/src/app/signup/page.tsx` around lines 38 - 39, Convert the click-only signup flow to a proper form submission: wrap the inputs and button in a <form> with onSubmit tied to handleSignup (replace button onClick), change the button to type="submit", and update handleSignup to accept an event parameter and call event.preventDefault() before running existing checks (isFormInvalid/isPending) and logic; apply the same change to the other similar block referenced (lines 131-137) so both flows use <form onSubmit> + type="submit" instead of click handlers.apps/customer/src/app/login/_components/LoginForm.tsx (1)
28-45: email/password 입력에 autoComplete를 추가해 주세요브라우저 자동완성/비밀번호 매니저 연동을 위해
current-password지정이 있으면 UX가 더 좋아집니다.수정 제안
<Input type="email" placeholder={LOGIN_TEXT.emailPlaceholder} value={values.email} + autoComplete="email" onChange={(e) => onChangeEmail(e.target.value)} /> @@ <Input type="password" placeholder={LOGIN_TEXT.passwordPlaceholder} value={values.password} + autoComplete="current-password" onChange={(e) => onChangePassword(e.target.value)} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer/src/app/login/_components/LoginForm.tsx` around lines 28 - 45, Add browser autocomplete attributes to the email and password Input components in LoginForm.tsx: set the email Input's autoComplete to "email" and the password Input's autoComplete to "current-password" so password managers and browser autofill work properly; update the two Input elements (the one using values.email and onChangeEmail, and the one using values.password and onChangePassword) to include these attributes while keeping existing props (placeholder, value, onChange).apps/customer/src/app/login/page.tsx (2)
70-72: 카카오 로그인 미구현 상태
handleKakaoLogin이console.log만 호출하는 플레이스홀더입니다. 의도된 것이라면 TODO 주석을 추가하거나, 버튼을 비활성화하는 것이 좋습니다.구현 예정이라면 TODO 주석을 추가하시겠습니까? 또는 카카오 OAuth 연동 코드를 생성해 드릴까요?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer/src/app/login/page.tsx` around lines 70 - 72, handleKakaoLogin is currently a placeholder that only logs to console; either implement the Kakao OAuth flow or make the placeholder explicit and safe. Add a TODO comment inside handleKakaoLogin noting the planned implementation, and update the corresponding Kakao login button to be disabled (e.g., disabled and aria-disabled) until real OAuth is implemented so users can't trigger a no-op; alternatively, if you prefer immediate implementation, replace handleKakaoLogin with the Kakao OAuth initiation (popup/redirect) and proper success/error handling. Ensure you modify the handleKakaoLogin function and the Kakao login button component referenced in the login page.
25-37: 선택적 개선: 중복 핸들러 통합 가능
handleChangeEmail과handleChangePassword는 동일한 패턴을 따르므로, 제네릭 핸들러로 통합할 수 있습니다.♻️ 제네릭 핸들러로 리팩토링 제안
- const handleChangeEmail = (value: string) => { - setFormValues((prev) => ({ - ...prev, - email: value, - })); - }; - - const handleChangePassword = (value: string) => { - setFormValues((prev) => ({ - ...prev, - password: value, - })); - }; + const handleChangeField = (field: keyof LoginReqDTO) => (value: string) => { + setFormValues((prev) => ({ + ...prev, + [field]: value, + })); + };JSX에서 사용:
onChangeEmail={handleChangeField("email")} onChangePassword={handleChangeField("password")}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer/src/app/login/page.tsx` around lines 25 - 37, handleChangeEmail and handleChangePassword are duplicate setters; replace them with a generic factory like handleChangeField(fieldName) that returns a handler which calls setFormValues(prev => ({ ...prev, [fieldName]: value })); update usages (e.g., onChangeEmail and onChangePassword) to call handleChangeField("email") and handleChangeField("password") respectively and remove the two specific functions (references: handleChangeEmail, handleChangePassword, setFormValues).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/customer/src/app/login/_components/LoginForm.tsx`:
- Around line 49-55: The LoginForm currently handles submission via Button
type="button" and onClick={onSubmit}, so pressing Enter in inputs won't submit;
modify the LoginForm component to wrap the username/password inputs in a <form>
and move the submission handler to the form (e.g., onSubmit={onSubmit}), then
change the Button in this diff from type="button" to type="submit" (keep
disabled={isPending}); alternatively you may add an onKeyDown Enter handler on
the input elements, but prefer the standard form approach so the existing
onSubmit handler is used by the form submission.
In `@apps/customer/src/app/login/page.tsx`:
- Around line 63-65: The onError handler in apps/customer/src/app/login/page.tsx
currently only console.logs the error; replace that with user-facing feedback by
updating the onError: (error) => { ... } handler to surface a clear message
(e.g., show a toast or set a local error state used by the UI) and map common
error cases (invalid credentials vs network/timeout vs unknown) to friendly
messages; use the existing toast/toastHook or a setState like loginError and
ensure the message includes error.message when appropriate and is cleared on
retry or success.
- Around line 43-46: Replace the console.log in the empty-field check with a
user-facing notification so users get immediate feedback: where the code checks
if (!email || !password) and currently calls
console.log(LOGIN_TEXT.emptyFieldMessage) (likely inside your submit/handleLogin
flow in page.tsx), call the app's UI feedback mechanism instead (e.g., show a
Toast, alert, or set an inline error state/variable bound to the form) using
LOGIN_TEXT.emptyFieldMessage so the message is visible to users and prevent
submission by returning as before.
In `@apps/customer/src/app/signup/page.tsx`:
- Around line 56-60: setSubmitError currently assigns error.message directly
which exposes internal errors; instead update the signup error handling in
page.tsx (where setSubmitError is called) to map known Error types/messages to
fixed user-facing strings and fall back to a single generic message like "회원가입 중
오류가 발생했습니다." Do this by inspecting the thrown value (e.g., error instanceof
Error ? error.message : ...) and translating it via a small switch/map before
calling setSubmitError, ensuring no raw error.message is passed through.
- Around line 44-50: Validation trims the inputs but the signUp call sends
original values, causing possible data mismatch; update the signUp invocation
(the call to signUp in page.tsx) to send the normalized/trimmed versions of the
fields (e.g., use trimmedName, trimmedNickname, trimmedEmail or call .trim() on
name, nickname, email before passing) so the transmitted payload matches the
earlier trim-based validation. Ensure the same normalization is applied to all
fields validated (memberName/name, nickname, email, etc.) and reuse the
normalized variables rather than the raw inputs.
In `@apps/customer/src/shared/api/api.ts`:
- Around line 10-31: The tokenStore implementation currently reads/writes access
and refresh tokens to localStorage (tokenStore, getAccessToken, getRefreshToken,
setTokens, clearTokens), which exposes them to XSS; change the flow so refresh
tokens are not stored in localStorage but instead are issued/cleared by the
server as HttpOnly Secure cookies (client JS must not read them), and keep
access tokens out of persistent storage (store in memory or short-lived
non-persistent storage) or use SameSite/HttpOnly cookies for both if moving
fully to cookie-based auth; update tokenStore methods: remove any client-side
reads/writes of the refresh token (getRefreshToken should stop returning
localStorage value and return null/undefined or be removed), make setTokens
avoid persisting refreshToken to localStorage (only set access token in memory),
and make clearTokens call a logout/clear-cookie endpoint on the server rather
than removing an HttpOnly cookie directly, plus update any code paths that
depended on localStorage refresh reads to use the server-side silent refresh
endpoint that relies on the HttpOnly cookie.
- Around line 34-37: The current compasserApi initialization uses
process.env.NEXT_PUBLIC_API_BASE_URL ?? "" which falls back to an empty string
and causes same-origin requests if the env is missing; change initialization to
fail-fast by validating NEXT_PUBLIC_API_BASE_URL before calling
createCompasserApi (e.g., throw an Error or assert) and pass the verified
non-empty baseURL into createCompasserApi (reference compasserApi and
createCompasserApi to locate the code); ensure the error message clearly states
the missing NEXT_PUBLIC_API_BASE_URL so startup fails immediately instead of
silently using an empty baseURL.
In `@apps/owner/src/app/`(tabs)/mypage/store-info/page.tsx:
- Around line 65-66: selectedTag (and tagOptions/setSelectedTag) is only used
for UI state and never sent to the server, so tag changes don't persist; update
the store-save flow (the form submit or save handler such as the function that
builds the save payload — e.g., handleSave/onSubmit/buildPayload) to include the
selectedTag value in the request payload by mapping the UI value to the API
field (e.g., { tag: selectedTag } or the API's expected enum/key), and ensure
the component initializes selectedTag from the server value as currently done;
if the backend API does not accept tag updates, hide or disable the tag selector
UI instead.
- Around line 57-63: The four fields storeEmail, bankName, depositor, and
bankAccount are initialized to empty strings and not seeded from myStore, so
saving can overwrite existing data with blanks; update the initialization to
derive initial state from myStore (e.g., useState(myStore?.storeEmail ?? "")
etc. for storeEmail, bankName, depositor, bankAccount) and/or change the save
handler (the function that builds the payload in the save/onSubmit handler) to
fall back to myStore values when the corresponding state is empty (e.g.,
payload.field = stateField || myStore?.field) so existing values aren’t lost;
adjust both the state declarations (storeEmail, bankName, depositor,
bankAccount) and the save payload construction to reference myStore where
appropriate.
In `@apps/owner/src/app/signup/page.tsx`:
- Around line 39-40: The code currently mutates the submitted credentials by
using formValues.password.trim() and formValues.passwordConfirm.trim(); instead,
keep the raw values (e.g., const password = formValues.password; const
passwordConfirm = formValues.passwordConfirm;) and create separate trimmed
versions only for validation (e.g., const passwordTrimmed = password.trim()).
Use the trimmed variables to perform empty/whitespace checks but send the
original password and passwordConfirm when constructing the payload or calling
the signup function (refer to password, passwordConfirm, passwordTrimmed,
formValues, and the signup handler in page.tsx); apply the same change to the
other similar blocks noted (lines 42-49, 53-60).
- Around line 42-50: The current checks that use console.log for missing fields
and API failures (references: memberName, nickname, email, password,
passwordConfirm and the signup submit handler where console.log is used) must be
replaced with a user-facing error state and display: add a local state like
errorMessage (or use a toast utility), set a clear, user-friendly message when
validation fails (e.g., "모든 항목을 입력해주세요.") instead of console.log, render that
message inline near the form or trigger a toast, and for API errors catch the
error, map it to a sanitized user message and set errorMessage (do not expose
raw error objects to users); keep only sanitized info in console logs for
debugging. Ensure both the client-side validation branch and the API catch block
update and display the same error UI.
In `@apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx`:
- Around line 62-69: handleSubmit currently fires onSubmit without guarding
against concurrent calls or rejections; add local isSubmitting and error state,
return early if isSubmitting is true, set isSubmitting = true before awaiting
onSubmit(form), wrap the await in try/catch to capture and set error state on
rejection, call onClose() only on successful completion, and in finally set
isSubmitting = false; apply the same change to the other submit handler around
lines 153-160 to prevent double submits and unhandled rejections.
In `@apps/owner/src/app/signup/register/_utils/business-hours.ts`:
- Around line 15-17: parseBusinessHours currently returns the shared
EMPTY_BUSINESS_HOURS object when input is empty, which reuses the same reference
across calls; instead, return a fresh copy to avoid shared-reference bugs in
React state and accidental mutations. Update parseBusinessHours to create and
return a new BusinessHoursValue (e.g., shallow/deep clone or construct a new
object with the same fields) rather than returning EMPTY_BUSINESS_HOURS
directly; ensure any nested arrays/objects are also copied if they can be
mutated so the returned value is independent of the constant.
In `@apps/owner/src/app/signup/register/page.tsx`:
- Around line 61-62: The selectedTag state (tagOptions, selectedTag,
setSelectedTag) is only kept in local component state and never persisted or
used in handleCompleteRegister, so the chosen tag never reaches the server;
either remove the tag UI or persist it by initializing selectedTag from existing
user/profile data on mount and include selectedTag in the registration payload
inside handleCompleteRegister (map the literal value to the server field
expected), and ensure any update paths (e.g., post-register patch or create-user
API call) save it so the value is restored on re-entry.
In `@apps/owner/src/shared/api/api.ts`:
- Around line 21-29: The setTokens function (TokenPair) leaves an old
ownerRefreshToken in localStorage when refreshToken is absent; update setTokens
so after setting ownerAccessToken it explicitly removes ownerRefreshToken
(localStorage.removeItem("ownerRefreshToken")) when refreshToken is falsy,
preserving the existing typeof window guard and current ownerAccessToken write
path to maintain consistency between tokens.
- Around line 38-40: The code silently allows an empty API base URL by
defaulting NEXT_PUBLIC_API_BASE_URL to "", so update the compasserApi
initialization to validate the environment variable and fail fast: in the
createCompasserApi call (compasserApi, createCompasserApi, tokenStore) check
process.env.NEXT_PUBLIC_API_BASE_URL is present and non-empty at startup and
throw or log a fatal error if missing (do not proceed with ""), so the app fails
loudly during initialization rather than making relative-path requests at
runtime.
In `@apps/owner/src/shared/stores/ownerSignup.store.ts`:
- Line 13: The store currently persists the email property to localStorage which
exposes PII; update the persist configuration used to create the owner signup
store (the persist(...) call that wraps this store) to exclude the email field
by using the partialize option and only persist non-PII progress/state keys,
keeping email as a normal in-memory property; apply the same change for the
other persisted store slice referenced around the code handling the fields at
61-63 so email is never written to storage.
In `@apps/owner/src/shared/utils/businessLicense.ts`:
- Around line 5-10: isValidBusinessNumber currently duplicates
isValidBusinessNumberFormat and only checks for 10 digits; replace its body to
perform the actual business number checksum validation (or remove it and use a
single function). Specifically, keep normalizeBusinessNumber and
isValidBusinessNumberFormat for format checking, and update
isValidBusinessNumber to: after normalizing and format-checking the value,
compute the checksum using the standard 10-digit business number algorithm (use
weights [1,3,7,1,3,7,1,3,5], add the carry from the 9th digit multiplication as
floor((digit9*5)/10), sum all products, then verify total % 10 === 0) so only
numbers that pass the checksum return true.
---
Outside diff comments:
In
`@apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx`:
- Around line 46-68: The delete <button> is nested inside the upload <button>
(the element using handleOpenFilePicker/previewUrl/Image), which violates HTML
specs; refactor so the delete control is not a child of the upload button: move
the delete control (currently rendering Icon and using handleClickRemove) out to
be a sibling element (e.g., a positioned <button> or <div role="button">)
adjacent to the upload <button>, preserve its styling and aria-label="사진 삭제",
and keep any event handlers (handleClickRemove) but remove reliance on
event.stopPropagation() to prevent nested-button issues; ensure keyboard focus
and onKey handlers remain functional for the new control.
---
Nitpick comments:
In `@apps/customer/src/app/login/_components/LoginForm.tsx`:
- Around line 28-45: Add browser autocomplete attributes to the email and
password Input components in LoginForm.tsx: set the email Input's autoComplete
to "email" and the password Input's autoComplete to "current-password" so
password managers and browser autofill work properly; update the two Input
elements (the one using values.email and onChangeEmail, and the one using
values.password and onChangePassword) to include these attributes while keeping
existing props (placeholder, value, onChange).
In `@apps/customer/src/app/login/page.tsx`:
- Around line 70-72: handleKakaoLogin is currently a placeholder that only logs
to console; either implement the Kakao OAuth flow or make the placeholder
explicit and safe. Add a TODO comment inside handleKakaoLogin noting the planned
implementation, and update the corresponding Kakao login button to be disabled
(e.g., disabled and aria-disabled) until real OAuth is implemented so users
can't trigger a no-op; alternatively, if you prefer immediate implementation,
replace handleKakaoLogin with the Kakao OAuth initiation (popup/redirect) and
proper success/error handling. Ensure you modify the handleKakaoLogin function
and the Kakao login button component referenced in the login page.
- Around line 25-37: handleChangeEmail and handleChangePassword are duplicate
setters; replace them with a generic factory like handleChangeField(fieldName)
that returns a handler which calls setFormValues(prev => ({ ...prev,
[fieldName]: value })); update usages (e.g., onChangeEmail and onChangePassword)
to call handleChangeField("email") and handleChangeField("password")
respectively and remove the two specific functions (references:
handleChangeEmail, handleChangePassword, setFormValues).
In `@apps/customer/src/app/signup/page.tsx`:
- Around line 38-39: Convert the click-only signup flow to a proper form
submission: wrap the inputs and button in a <form> with onSubmit tied to
handleSignup (replace button onClick), change the button to type="submit", and
update handleSignup to accept an event parameter and call event.preventDefault()
before running existing checks (isFormInvalid/isPending) and logic; apply the
same change to the other similar block referenced (lines 131-137) so both flows
use <form onSubmit> + type="submit" instead of click handlers.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0465f3e3-38b2-45c3-8668-36f2be3f0e05
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (42)
apps/customer/src/app/login/_components/KakaoLoginButton.tsxapps/customer/src/app/login/_components/LoginForm.tsxapps/customer/src/app/login/_components/SignupLinkButton.tsxapps/customer/src/app/login/_constants/login.constants.tsapps/customer/src/app/login/page.tsxapps/customer/src/app/signup/page.tsxapps/customer/src/shared/api/api.tsapps/customer/src/shared/queries/mutation/auth/useLoginMutation.tsapps/customer/src/shared/queries/mutation/auth/useSignupMutation.tsapps/owner/src/app/(tabs)/mypage/store-info/page.tsxapps/owner/src/app/signup/business/page.tsxapps/owner/src/app/signup/page.tsxapps/owner/src/app/signup/register/_components/fields/AccountField.tsxapps/owner/src/app/signup/register/_components/fields/EmailField.tsxapps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsxapps/owner/src/app/signup/register/_components/fields/StoreNameField.tsxapps/owner/src/app/signup/register/_components/modals/BusinessHoursModal.tsxapps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsxapps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsxapps/owner/src/app/signup/register/_components/sections/RandomBoxSection.tsxapps/owner/src/app/signup/register/_components/sections/TagSection.tsxapps/owner/src/app/signup/register/_constants/register.tsapps/owner/src/app/signup/register/_types/register.tsapps/owner/src/app/signup/register/_utils/business-hours.tsapps/owner/src/app/signup/register/page.tsxapps/owner/src/shared/api/api.tsapps/owner/src/shared/queries/mutation/auth/useCreateRandomBoxMutation.tsapps/owner/src/shared/queries/mutation/auth/useDeleteRandomBoxMutation.tsapps/owner/src/shared/queries/mutation/auth/usePatchMyStoreLocationMutation.tsapps/owner/src/shared/queries/mutation/auth/usePatchMyStoreMutation.tsapps/owner/src/shared/queries/mutation/auth/useRemoveStoreImageMutation.tsapps/owner/src/shared/queries/mutation/auth/useSignupMutation.tsapps/owner/src/shared/queries/mutation/auth/useUpdateRandomBoxMutation.tsapps/owner/src/shared/queries/mutation/auth/useUploadStoreImageMutation.tsapps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.tsapps/owner/src/shared/queries/query/useMyStoreQuery.tsapps/owner/src/shared/queries/query/useRandomBoxListQuery.tsapps/owner/src/shared/queries/query/useStoreImageQuery.tsapps/owner/src/shared/stores/ownerSignup.store.tsapps/owner/src/shared/utils/bank.tsapps/owner/src/shared/utils/businessLicense.tspackage.json
💤 Files with no reviewable changes (2)
- apps/owner/src/app/signup/register/_types/register.ts
- apps/owner/src/app/signup/register/_constants/register.ts
| <Button | ||
| type="button" | ||
| size="lg" | ||
| kind="default" | ||
| variant="primary" | ||
| onClick={onSubmit} | ||
| disabled={isPending} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# LoginForm 사용처에서 form submit 연결 여부 확인
rg -n -C3 'LoginForm' apps/customer/src/app/login
rg -n -C3 '<form|onSubmit=|type="submit"' apps/customer/src/app/loginRepository: CUK-Compasser/FE
Length of output: 3382
🏁 Script executed:
cat -n apps/customer/src/app/login/_components/LoginForm.tsxRepository: CUK-Compasser/FE
Length of output: 2103
🏁 Script executed:
cat -n apps/customer/src/app/login/page.tsx | head -100Repository: CUK-Compasser/FE
Length of output: 3148
🏁 Script executed:
rg -n 'form|Form' apps/customer/src/app/login/_components/LoginForm.tsxRepository: CUK-Compasser/FE
Length of output: 151
LoginForm이 Enter 키 제출을 지원하지 않습니다
현재 구현에서 LoginForm은 <form> 요소로 감싸져 있지 않고, Button의 type="button"과 onClick={onSubmit} 조합으로만 제출을 처리합니다. 따라서 비밀번호 입력 필드에서 Enter 키를 눌러도 폼이 제출되지 않으며, 오직 버튼 클릭으로만 로그인이 가능합니다.
표준 HTML 폼 동작에 대한 사용자 기대치를 벗어나므로 다음 중 하나로 개선이 필요합니다:
<form>요소로 inputs을 감싸고 Button의type="submit"으로 변경- 또는 Input 요소들에 Enter 키 이벤트 핸들러(onKeyDown) 추가
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/customer/src/app/login/_components/LoginForm.tsx` around lines 49 - 55,
The LoginForm currently handles submission via Button type="button" and
onClick={onSubmit}, so pressing Enter in inputs won't submit; modify the
LoginForm component to wrap the username/password inputs in a <form> and move
the submission handler to the form (e.g., onSubmit={onSubmit}), then change the
Button in this diff from type="button" to type="submit" (keep
disabled={isPending}); alternatively you may add an onKeyDown Enter handler on
the input elements, but prefer the standard form approach so the existing
onSubmit handler is used by the form submission.
| if (!email || !password) { | ||
| console.log(LOGIN_TEXT.emptyFieldMessage); | ||
| return; | ||
| } |
There was a problem hiding this comment.
사용자에게 유효성 검사 실패 피드백 필요
console.log는 개발 중에만 확인 가능하며, 실제 사용자는 이메일/비밀번호가 비어있을 때 아무런 피드백을 받지 못합니다. Toast, alert, 또는 inline error message를 통해 사용자에게 알려야 합니다.
🛠️ 사용자 피드백 추가 제안
if (!email || !password) {
- console.log(LOGIN_TEXT.emptyFieldMessage);
+ // TODO: Toast 또는 alert로 사용자에게 피드백 제공
+ alert(LOGIN_TEXT.emptyFieldMessage);
return;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/customer/src/app/login/page.tsx` around lines 43 - 46, Replace the
console.log in the empty-field check with a user-facing notification so users
get immediate feedback: where the code checks if (!email || !password) and
currently calls console.log(LOGIN_TEXT.emptyFieldMessage) (likely inside your
submit/handleLogin flow in page.tsx), call the app's UI feedback mechanism
instead (e.g., show a Toast, alert, or set an inline error state/variable bound
to the form) using LOGIN_TEXT.emptyFieldMessage so the message is visible to
users and prevent submission by returning as before.
| onError: (error) => { | ||
| console.log("일반 로그인 실패", error); | ||
| }, |
There was a problem hiding this comment.
로그인 실패 시 사용자 피드백 필요
로그인 실패 시 console.log만 호출되어 사용자는 실패 원인을 알 수 없습니다. 잘못된 자격 증명, 네트워크 오류 등 상황에 맞는 에러 메시지를 사용자에게 표시해야 합니다.
🛠️ 에러 피드백 추가 제안
onError: (error) => {
- console.log("일반 로그인 실패", error);
+ console.error("일반 로그인 실패", error);
+ // TODO: 사용자에게 에러 메시지 표시
+ alert("로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.");
},📝 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.
| onError: (error) => { | |
| console.log("일반 로그인 실패", error); | |
| }, | |
| onError: (error) => { | |
| console.error("일반 로그인 실패", error); | |
| // TODO: 사용자에게 에러 메시지 표시 | |
| alert("로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요."); | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/customer/src/app/login/page.tsx` around lines 63 - 65, The onError
handler in apps/customer/src/app/login/page.tsx currently only console.logs the
error; replace that with user-facing feedback by updating the onError: (error)
=> { ... } handler to surface a clear message (e.g., show a toast or set a local
error state used by the UI) and map common error cases (invalid credentials vs
network/timeout vs unknown) to friendly messages; use the existing
toast/toastHook or a setState like loginError and ensure the message includes
error.message when appropriate and is cleared on retry or success.
| await signUp({ | ||
| memberName: name, | ||
| nickname, | ||
| email, | ||
| password, | ||
| passwordConfirm, | ||
| }); |
There was a problem hiding this comment.
검증은 trim인데 전송은 원본 값이라 데이터 불일치가 발생할 수 있습니다
Line 29-36에서 trim 검증을 하므로, Line 44-49 전송값도 동일하게 정규화해 보내는 편이 안전합니다.
수정 제안
await signUp({
- memberName: name,
- nickname,
- email,
+ memberName: name.trim(),
+ nickname: nickname.trim(),
+ email: email.trim(),
password,
passwordConfirm,
});📝 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.
| await signUp({ | |
| memberName: name, | |
| nickname, | |
| email, | |
| password, | |
| passwordConfirm, | |
| }); | |
| await signUp({ | |
| memberName: name.trim(), | |
| nickname: nickname.trim(), | |
| email: email.trim(), | |
| password, | |
| passwordConfirm, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/customer/src/app/signup/page.tsx` around lines 44 - 50, Validation trims
the inputs but the signUp call sends original values, causing possible data
mismatch; update the signUp invocation (the call to signUp in page.tsx) to send
the normalized/trimmed versions of the fields (e.g., use trimmedName,
trimmedNickname, trimmedEmail or call .trim() on name, nickname, email before
passing) so the transmitted payload matches the earlier trim-based validation.
Ensure the same normalization is applied to all fields validated
(memberName/name, nickname, email, etc.) and reuse the normalized variables
rather than the raw inputs.
| setSubmitError( | ||
| error instanceof Error | ||
| ? error.message | ||
| : "회원가입 중 오류가 발생했습니다.", | ||
| ); |
There was a problem hiding this comment.
원본 에러 메시지 직접 노출은 피하는 것이 좋습니다
Line 56-60에서 error.message를 그대로 보여주면 내부 메시지가 사용자에게 노출될 수 있습니다. 사용자 메시지는 고정/매핑 기반으로 제한하는 것을 권장합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/customer/src/app/signup/page.tsx` around lines 56 - 60, setSubmitError
currently assigns error.message directly which exposes internal errors; instead
update the signup error handling in page.tsx (where setSubmitError is called) to
map known Error types/messages to fixed user-facing strings and fall back to a
single generic message like "회원가입 중 오류가 발생했습니다." Do this by inspecting the
thrown value (e.g., error instanceof Error ? error.message : ...) and
translating it via a small switch/map before calling setSubmitError, ensuring no
raw error.message is passed through.
| const tagOptions = ["카페", "베이커리", "식당"] as const; | ||
| const [selectedTag, setSelectedTag] = useState<"" | "카페" | "베이커리" | "식당">(""); |
There was a problem hiding this comment.
회원가입 완료 시 선택한 태그가 전혀 저장되지 않습니다.
selectedTag는 로컬 state로만 유지되고, 재진입 시에도 복원되지 않으며, handleCompleteRegister에서도 사용되지 않습니다. 지금 UI에서는 태그를 고를 수 있지만 서버 데이터에는 반영되지 않습니다. 등록 단계에서 필요 없는 정보라면 섹션을 제거하고, 필요하다면 저장 payload에 매핑해 주세요.
Also applies to: 71-78, 188-199, 255-259
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/owner/src/app/signup/register/page.tsx` around lines 61 - 62, The
selectedTag state (tagOptions, selectedTag, setSelectedTag) is only kept in
local component state and never persisted or used in handleCompleteRegister, so
the chosen tag never reaches the server; either remove the tag UI or persist it
by initializing selectedTag from existing user/profile data on mount and include
selectedTag in the registration payload inside handleCompleteRegister (map the
literal value to the server field expected), and ensure any update paths (e.g.,
post-register patch or create-user API call) save it so the value is restored on
re-entry.
| setTokens: ({ accessToken, refreshToken }: TokenPair) => { | ||
| if (typeof window === "undefined") return; | ||
|
|
||
| localStorage.setItem("ownerAccessToken", accessToken); | ||
|
|
||
| if (refreshToken) { | ||
| localStorage.setItem("ownerRefreshToken", refreshToken); | ||
| } | ||
| }, |
There was a problem hiding this comment.
refreshToken 누락 시 이전 토큰이 남는 결함이 있습니다.
현재는 refreshToken이 없을 때 기존 ownerRefreshToken을 삭제하지 않아, 이전 세션 토큰이 잔존할 수 있습니다. 토큰 쌍 일관성이 깨져 인증 오류/오염 상태를 유발할 수 있습니다.
수정 예시
setTokens: ({ accessToken, refreshToken }: TokenPair) => {
if (typeof window === "undefined") return;
localStorage.setItem("ownerAccessToken", accessToken);
if (refreshToken) {
localStorage.setItem("ownerRefreshToken", refreshToken);
+ } else {
+ localStorage.removeItem("ownerRefreshToken");
}
},📝 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.
| setTokens: ({ accessToken, refreshToken }: TokenPair) => { | |
| if (typeof window === "undefined") return; | |
| localStorage.setItem("ownerAccessToken", accessToken); | |
| if (refreshToken) { | |
| localStorage.setItem("ownerRefreshToken", refreshToken); | |
| } | |
| }, | |
| setTokens: ({ accessToken, refreshToken }: TokenPair) => { | |
| if (typeof window === "undefined") return; | |
| localStorage.setItem("ownerAccessToken", accessToken); | |
| if (refreshToken) { | |
| localStorage.setItem("ownerRefreshToken", refreshToken); | |
| } else { | |
| localStorage.removeItem("ownerRefreshToken"); | |
| } | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/owner/src/shared/api/api.ts` around lines 21 - 29, The setTokens
function (TokenPair) leaves an old ownerRefreshToken in localStorage when
refreshToken is absent; update setTokens so after setting ownerAccessToken it
explicitly removes ownerRefreshToken
(localStorage.removeItem("ownerRefreshToken")) when refreshToken is falsy,
preserving the existing typeof window guard and current ownerAccessToken write
path to maintain consistency between tokens.
| export const compasserApi = createCompasserApi({ | ||
| baseURL: process.env.NEXT_PUBLIC_API_BASE_URL ?? "", | ||
| tokenStore, |
There was a problem hiding this comment.
API baseURL 미설정을 조용히 허용하지 마세요.
NEXT_PUBLIC_API_BASE_URL이 없을 때 ""로 진행하면 상대 경로 호출로 이어져 환경 오설정이 런타임에서 늦게 드러납니다. 초기화 시점에 명시적으로 실패시키는 편이 안전합니다.
수정 예시
-export const compasserApi = createCompasserApi({
- baseURL: process.env.NEXT_PUBLIC_API_BASE_URL ?? "",
- tokenStore,
-});
+const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL;
+if (!baseURL) {
+ throw new Error("NEXT_PUBLIC_API_BASE_URL is required");
+}
+
+export const compasserApi = createCompasserApi({
+ baseURL,
+ tokenStore,
+});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/owner/src/shared/api/api.ts` around lines 38 - 40, The code silently
allows an empty API base URL by defaulting NEXT_PUBLIC_API_BASE_URL to "", so
update the compasserApi initialization to validate the environment variable and
fail fast: in the createCompasserApi call (compasserApi, createCompasserApi,
tokenStore) check process.env.NEXT_PUBLIC_API_BASE_URL is present and non-empty
at startup and throw or log a fatal error if missing (do not proceed with ""),
so the app fails loudly during initialization rather than making relative-path
requests at runtime.
| signupCompleted: boolean; | ||
| businessCompleted: boolean; | ||
| registerCompleted: boolean; | ||
| email: string | null; |
There was a problem hiding this comment.
이메일을 영구 저장하지 않는 쪽이 안전합니다.
현재 email이 persist 대상이라 localStorage에 남습니다. 공유 기기/장기 잔존 관점에서 PII 노출 위험이 있어, partialize로 진행 상태만 저장하고 이메일은 메모리 상태로 유지하는 구성이 더 안전합니다.
수정 예시
export const useOwnerSignupStore = create<OwnerSignupState>()(
persist(
(set) => ({
step: "signup",
signupCompleted: false,
businessCompleted: false,
registerCompleted: false,
email: null,
@@
{
name: "owner-signup-flow",
+ partialize: (state) => ({
+ step: state.step,
+ signupCompleted: state.signupCompleted,
+ businessCompleted: state.businessCompleted,
+ registerCompleted: state.registerCompleted,
+ }),
},
),
);Also applies to: 61-63
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/owner/src/shared/stores/ownerSignup.store.ts` at line 13, The store
currently persists the email property to localStorage which exposes PII; update
the persist configuration used to create the owner signup store (the
persist(...) call that wraps this store) to exclude the email field by using the
partialize option and only persist non-PII progress/state keys, keeping email as
a normal in-memory property; apply the same change for the other persisted store
slice referenced around the code handling the fields at 61-63 so email is never
written to storage.
| export const isValidBusinessNumberFormat = (value: string) => { | ||
| return /^\d{10}$/.test(normalizeBusinessNumber(value)); | ||
| }; | ||
|
|
||
| export const isValidBusinessNumber = (value: string) => { | ||
| return /^\d{10}$/.test(normalizeBusinessNumber(value)); |
There was a problem hiding this comment.
isValidBusinessNumber가 현재는 형식 검사와 동일합니다.
이 구현이면 사업자 인증 페이지의 두 번째 검증 분기가 사실상 도달 불가입니다. 지금은 10자리 숫자이기만 하면 모두 통과하므로, 실제 유효성 검사(체크섬)를 넣거나 함수 하나로 정리해야 합니다.
수정 예시
+const BUSINESS_NUMBER_REGEX = /^\d{10}$/;
+
export const normalizeBusinessNumber = (value: string) => {
return value.replace(/\D/g, "");
};
export const isValidBusinessNumberFormat = (value: string) => {
- return /^\d{10}$/.test(normalizeBusinessNumber(value));
+ return BUSINESS_NUMBER_REGEX.test(normalizeBusinessNumber(value));
};
export const isValidBusinessNumber = (value: string) => {
- return /^\d{10}$/.test(normalizeBusinessNumber(value));
+ const normalized = normalizeBusinessNumber(value);
+ if (!BUSINESS_NUMBER_REGEX.test(normalized)) return false;
+
+ const digits = normalized.split("").map(Number);
+ const weights = [1, 3, 7, 1, 3, 7, 1, 3];
+ const weightedSum = weights.reduce(
+ (sum, weight, index) => sum + digits[index] * weight,
+ 0,
+ );
+ const ninthProduct = digits[8] * 5;
+ const checksum =
+ (10 -
+ ((weightedSum +
+ Math.floor(ninthProduct / 10) +
+ (ninthProduct % 10)) %
+ 10)) %
+ 10;
+
+ return checksum === digits[9];
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/owner/src/shared/utils/businessLicense.ts` around lines 5 - 10,
isValidBusinessNumber currently duplicates isValidBusinessNumberFormat and only
checks for 10 digits; replace its body to perform the actual business number
checksum validation (or remove it and use a single function). Specifically, keep
normalizeBusinessNumber and isValidBusinessNumberFormat for format checking, and
update isValidBusinessNumber to: after normalizing and format-checking the
value, compute the checksum using the standard 10-digit business number
algorithm (use weights [1,3,7,1,3,7,1,3,5], add the carry from the 9th digit
multiplication as floor((digit9*5)/10), sum all products, then verify total % 10
=== 0) so only numbers that pass the checksum return true.
✅ 작업 내용
📝 Description
🚀 설계 의도 및 개선점
@compasser/api) 기반으로 데이터 흐름 일원화📎 기타 참고사항
Fixes #81
Summary by CodeRabbit
릴리스 노트
새 기능
개선사항