Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
890 changes: 870 additions & 20 deletions package-lock.json

Large diffs are not rendered by default.

6,354 changes: 6,354 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions src/api/auth/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { TChangeNicknamePayload, TChangeNicknameResponse, TChangePasswordPayload, TGetMemberGradeResponse, TMemberInfo } from '@/types/auth/account';
import type { TCommonResponse } from '@/types/common/common';

import { axiosInstance } from '@/api/axiosInstance';

// 비밀번호 변경
export async function changePassword(payload: TChangePasswordPayload): Promise<void> {
const body = { nowPassword: payload.currentPassword, newPassword: payload.newPassword };
await axiosInstance.patch('/api/v1/members/passwords', body);
}

// 닉네임 변경
export async function changeNickname(payload: TChangeNicknamePayload): Promise<TChangeNicknameResponse> {
const { data } = await axiosInstance.patch<TChangeNicknameResponse>('/api/v1/members/infos', payload);
return data;
}

// 탈퇴
export async function deleteMember(): Promise<void> {
await axiosInstance.delete('/api/v1/members');
}

// 사용자 정보 조회
export async function getMemberInfo(): Promise<TCommonResponse<TMemberInfo>> {
const { data } = await axiosInstance.get<TCommonResponse<TMemberInfo>>('/api/v1/members/infos');
return data;
}

// 사용자 등급 조회
export async function getMemberGrade(): Promise<TGetMemberGradeResponse> {
const { data } = await axiosInstance.get<TGetMemberGradeResponse>('/api/v1/members/grade');
return data;
}
9 changes: 9 additions & 0 deletions src/api/dates/preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { TResetPreferencesResponse } from '@/types/dates/preferences';

import { axiosInstance } from '../axiosInstance';

// 취향 데이터 초기화
export async function resetPreferences(): Promise<TResetPreferencesResponse> {
const { data } = await axiosInstance.delete<TResetPreferencesResponse>('/api/v1/dates/preferences');
return data;
}
5 changes: 5 additions & 0 deletions src/api/faq/faq.keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const faqKeys = {
all: ['faqs'] as const,
list: (p: { category: string; page: number; size: number }) => [...faqKeys.all, 'list', p] as const,
search: (p: { keyword: string; category?: string; page: number; size: number }) => [...faqKeys.all, 'search', p] as const,
};
Comment on lines +1 to +5
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

키 빌더 패턴 적절합니다. 파라미터 타입 재사용을 검토해 주세요

구조/리터럴 타입 보존(as const)도 적절합니다. 다만 list, search의 파라미터 구조가 다른 곳에서도 쓰인다면 공용 타입(예: TFaqListParams, TFaqSearchParams)으로 분리/재사용하면 추후 변경 시 추적이 용이합니다.

🤖 Prompt for AI Agents
In src/api/faq/faq.keys.ts around lines 1 to 5, the inline parameter object
types for list and search should be extracted into shared TypeScript types for
reuse and maintainability; create exported types (e.g. TFaqListParams and
TFaqSearchParams) that capture the existing shapes (list: { category: string;
page: number; size: number }, search: { keyword: string; category?: string;
page: number; size: number }), replace the inline annotations in faqKeys.list
and faqKeys.search with these new types, and export the types so other modules
can import and reuse them.

28 changes: 28 additions & 0 deletions src/api/faq/faq.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { TFaqCategory, TFetchFaqsResponse } from '@/types/faq/faq';

import { axiosInstance } from '@/api/axiosInstance';

// FAQ 목록 조회
export const getFaqs = async (params: { category: TFaqCategory; page: number; size: number }) => {
const { data } = await axiosInstance.get<TFetchFaqsResponse>('/api/v1/faqs', {
params: {
faqCategory: params.category,
page: params.page,
size: params.size,
},
});
return data;
};

// FAQ 검색
export const searchFaqs = async (params: { keyword: string; category?: TFaqCategory; page: number; size: number }) => {
const { data } = await axiosInstance.get<TFetchFaqsResponse>('/api/v1/faqs/search', {
params: {
keyword: params.keyword,
faqCategory: params.category,
page: params.page,
size: params.size,
},
});
return data;
};
12 changes: 12 additions & 0 deletions src/api/notice/notice.keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const noticeKeys = {
root: ['notice'] as const, // 루트 키

// 목록 조회 키
list: (p: { category: 'SERVICE' | 'SYSTEM'; page: number; size: number }) => [...noticeKeys.root, 'list', p] as const,

// 상세 조회 키
detail: (id: number) => [...noticeKeys.root, 'detail', id] as const,

// 검색 조회 키
search: (p: { keyword: string; page: number; size: number; category?: 'SERVICE' | 'SYSTEM' }) => [...noticeKeys.root, 'search', p] as const,
};
Comment on lines +1 to +12
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Notice 키 정의가 두 곳으로 분산되어 있습니다 — 단일 소스로 통합 권장

relevant_code_snippets에 따르면 기존에도 NoticeKeys(getAllNotices 등)가 존재합니다. 키 스키마가 중복되면 캐시 충돌/이중 관리가 생깁니다. notice.keys.ts를 단일 소스로 승격하고 기존 키는 deprecated 처리 또는 래핑으로 일원화해 주세요.

레거시 키 사용처를 빠르게 확인하려면:


🏁 Script executed:

#!/bin/bash
# 레거시 NoticeKeys 사용처 탐색
rg -n -A 2 "NoticeKeys\."
# 신규 noticeKeys 사용처 탐색
rg -n -A 2 "noticeKeys\."

Length of output: 1895


🏁 Script executed:

#!/bin/bash
# 레거시 NoticeKeys 정의 위치 탐색
rg -n "export const NoticeKeys" -C 2
rg -n "NoticeKeys\s*=\s*{" -C 2

Length of output: 527


🏁 Script executed:

#!/bin/bash
# NoticeKeys 전체 정의 블록 확인
rg -n "export const NoticeKeys" -A 20 src/queryKey/queryKey.ts

Length of output: 290


Notice 키 정의 단일화 필요
현재 noticeKeys(src/api/notice/notice.keys.ts)와 레거시 NoticeKeys(src/queryKey/queryKey.ts)에 키가 중복되어 관리되고 있습니다. 이로 인해 캐시 충돌이나 이중 관리 리스크가 발생할 수 있습니다. 아래 파일을 하나의 소스로 통합하고, 레거시 정의는 deprecated 처리 또는 새 정의로 래핑해 주세요.

영향 받는 위치:

  • src/queryKey/queryKey.ts
    export const NoticeKeys = createQueryKeys('notice', { … })
  • src/api/notice/notice.keys.ts
    export const noticeKeys = { root, list, detail, search }
  • src/hooks/notices/useGetNotices.ts
    NoticeKeys.getAllNotices(...) 사용
  • src/hooks/notice/useNotice.ts
    noticeKeys.list(...), noticeKeys.detail(...), noticeKeys.search(...) 사용

예시(diff 제안):

// src/queryKey/queryKey.ts
-export const NoticeKeys = createQueryKeys('notice', {
-  all: () => ['notice'],
-  getAllNotices: (page, size, noticeCategory) => ['notice', page, size, noticeCategory],
-});
+export const noticeKeys = createQueryKeys('notice', {
+  root: () => ['notice'],
+  list: (p: { category: 'SERVICE' | 'SYSTEM'; page: number; size: number }) =>
+    [...root(), 'list', p] as const,
+  detail: (id: number) =>
+    [...root(), 'detail', id] as const,
+  search: (p: { keyword: string; page: number; size: number; category?: 'SERVICE' | 'SYSTEM' }) =>
+    [...root(), 'search', p] as const,
+});
  • 기존 NoticeKeys.getAllNotices 사용부는 noticeKeys.list로 교체
  • 레거시 키에는 /** @deprecated use noticeKeys.list */ 주석 추가

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

🤖 Prompt for AI Agents
In src/api/notice/notice.keys.ts (lines 1-12) and the legacy
src/queryKey/queryKey.ts, unify the notice key definitions by making
src/api/notice/notice.keys.ts the single source of truth: export noticeKeys as
the canonical object and change the legacy NoticeKeys export to be a thin
wrapper that delegates to noticeKeys (e.g., map NoticeKeys.getAllNotices →
noticeKeys.list, getNotice → noticeKeys.detail, search → noticeKeys.search) with
a /** @deprecated use noticeKeys.* */ comment on each legacy member; then update
usages (src/hooks/notices/useGetNotices.ts and src/hooks/notice/useNotice.ts) to
import and use noticeKeys.list/detail/search respectively, remove duplicate
logic from the legacy file or leave it delegating to noticeKeys, and ensure
typings/signatures match so no callsites need edits beyond import replacement.

35 changes: 27 additions & 8 deletions src/api/notice/notice.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
import type { TFetchNoticeDetailResponse, TFetchNoticesResponse, TRequestGetNoticeRequest } from '@/types/notice/notice';
import type { TFetchNoticeDetailResponse, TFetchNoticesResponse } from '@/types/notice/notice';

import { axiosInstance } from '@/api/axiosInstance';

// 공지사항 전체 조회 API
export const fetchNotices = async ({ noticeCategory = 'SERVICE', page, size }: TRequestGetNoticeRequest): Promise<TFetchNoticesResponse> => {
const { data } = await axiosInstance.get('/api/v1/notices', {
params: { noticeCategory: noticeCategory, page, size },
// 공지사항 전체 조회
export const getNotices = async (params: { category: 'SERVICE' | 'SYSTEM'; page: number; size: number }) => {
const { data } = await axiosInstance.get<TFetchNoticesResponse>('/api/v1/notices', {
params: {
noticeCategory: params.category,
page: params.page,
size: params.size,
},
});
return data;
};

// 공지사항 상세 조회 API
export const fetchNoticeDetail = async (noticeId: number): Promise<TFetchNoticeDetailResponse> => {
const { data } = await axiosInstance.get(`/api/v1/notices/${noticeId}`);
// 상세 조회
export const getNoticeDetail = async (noticeId: number) => {
const { data } = await axiosInstance.get<TFetchNoticeDetailResponse>(`/api/v1/notices/${noticeId}`);
return data;
};

// 공지 검색
export const searchNotices = async (params: { keyword: string; page: number; size: number; category?: 'SERVICE' | 'SYSTEM' }) => {
const { keyword, page, size, category } = params;

const { data } = await axiosInstance.get<TFetchNoticesResponse>('/api/v1/notices/search', {
params: {
keyword,
page,
size,
...(category && { noticeCategory: category }),
},
});
return data;
};
15 changes: 15 additions & 0 deletions src/api/settingAlarm/alarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { TAlarmSettings, TGetAlarmSettingsResp, TPatchAlarmSettingsResp } from '@/types/settingAlarm/alarm';

import { axiosInstance } from '@/api/axiosInstance';

// 조회
export async function getAlarmSettings(): Promise<TGetAlarmSettingsResp> {
const { data } = await axiosInstance.get<TGetAlarmSettingsResp>('/api/v1/alarms/settings');
return data;
}

// 업데이트
export async function patchAlarmSettings(payload: TAlarmSettings): Promise<TPatchAlarmSettingsResp> {
const { data } = await axiosInstance.patch<TPatchAlarmSettingsResp>('/api/v1/alarms/settings', payload);
return data;
}
56 changes: 45 additions & 11 deletions src/components/common/EditableInputBox.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//setting - common input box
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';

import SearchIcon from '@/assets/icons/Search_Blank.svg?react';

Expand All @@ -15,10 +14,14 @@ interface IEditableInputBoxProps {
onSearchClick?: () => void;
className?: string;
placeholder?: string;
readOnly?: boolean;
onEditStart?: () => void;
onFocus?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
}

export default function EditableInputBox({
mode = 'default',
type,
label = '',
value,
onChange,
Expand All @@ -28,12 +31,29 @@ export default function EditableInputBox({
onSearchClick,
className = '',
placeholder = '',
readOnly = false,
onEditStart,
onFocus,
}: IEditableInputBoxProps) {
const [isEditing, setIsEditing] = useState(false);

const isNickname = mode === 'nickname';
const isSearch = mode === 'search';

const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);

// 편집 모드 - 자동 포커스 + 전체 선택
useEffect(() => {
const el = inputRef.current;
if (isNickname && isEditing && el) {
const frameId = requestAnimationFrame(() => {
el.focus();
(el as HTMLInputElement | HTMLTextAreaElement).select();
});
return () => cancelAnimationFrame(frameId);
}
}, [isNickname, isEditing]);

const handleCancel = () => {
setIsEditing(false);
onCancel?.();
Expand Down Expand Up @@ -69,48 +89,62 @@ export default function EditableInputBox({
{label && <p className="font-body1 text-default-gray-700 mb-1">{label}</p>}

<div className="relative w-full">
{/* 닉네임 - 수정 중일 때 textarea */}
{/* 닉네임 */}
{isNickname && isEditing ? (
<textarea value={value} onChange={onChange} placeholder={placeholder} maxLength={maxLength} className={`${sharedClassName} resize-none`} />
<textarea
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
value={value}
onChange={onChange}
placeholder={placeholder}
maxLength={maxLength}
className={`${sharedClassName} resize-none`}
onFocus={onFocus}
/>
Comment on lines +94 to +102
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

readOnly=true일 때 닉네임 편집이 막히지 않습니다

textarea에는 readOnly가 전달되지 않아 편집이 가능해집니다. 읽기 전용 모드에서 편집 진입 자체도 차단하는 것이 일관됩니다.

                 {isNickname && isEditing ? (
                     <textarea
                         ref={inputRef as React.RefObject<HTMLTextAreaElement>}
                         value={value}
                         onChange={onChange}
                         placeholder={placeholder}
                         maxLength={maxLength}
+                        readOnly={readOnly}
                         className={`${sharedClassName} resize-none`}
                         onFocus={onFocus}
                     />
📝 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
<textarea
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
value={value}
onChange={onChange}
placeholder={placeholder}
maxLength={maxLength}
className={`${sharedClassName} resize-none`}
onFocus={onFocus}
/>
<textarea
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
value={value}
onChange={onChange}
placeholder={placeholder}
maxLength={maxLength}
readOnly={readOnly}
className={`${sharedClassName} resize-none`}
onFocus={onFocus}
/>
🤖 Prompt for AI Agents
In src/components/common/EditableInputBox.tsx around lines 94-102, the textarea
is missing the readOnly prop so the field remains editable even when
readOnly=true; add readOnly={readOnly} and aria-readonly={readOnly} to the
textarea, and prevent entering edit mode by not calling the onFocus handler when
readOnly is true (pass undefined or a conditional wrapper), ensuring onChange
cannot mutate value in read-only mode.

) : (
<input
type="text"
ref={inputRef as React.RefObject<HTMLInputElement>}
type={type ?? 'text'}
value={value}
onChange={onChange}
readOnly={isNickname ? !isEditing : false}
readOnly={readOnly || (isNickname ? !isEditing : false)}
placeholder={placeholder}
maxLength={maxLength}
onKeyDown={handleKeyDown}
className={sharedClassName}
onFocus={onFocus}
/>
)}

{/* 수정 버튼 */}
{/* 수정 */}
{isNickname && !isEditing && (
<button
onClick={() => setIsEditing(true)}
type="button"
onClick={() => {
setIsEditing(true);
onEditStart?.();
}}
className="absolute right-3 top-1/2 -translate-y-1/2 font-body1 px-3 py-1 rounded-full bg-default-gray-400 text-default-gray-700"
Comment on lines +118 to 126
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

읽기 전용 모드에서 '수정' 버튼 비활성화/가드 추가

readOnly=true에서도 수정 버튼이 동작합니다. 비활성화와 클릭 가드를 함께 적용해 주세요.

                 {isNickname && !isEditing && (
                     <button
-                        type="button"
-                        onClick={() => {
-                            setIsEditing(true);
-                            onEditStart?.();
-                        }}
+                        type="button"
+                        disabled={readOnly}
+                        onClick={() => {
+                            if (readOnly) return;
+                            setIsEditing(true);
+                            onEditStart?.();
+                        }}
                         className="absolute right-3 top-1/2 -translate-y-1/2 font-body1 px-3 py-1 rounded-full bg-default-gray-400 text-default-gray-700"
                     >
                         수정
                     </button>
                 )}
📝 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
{/* 수정 */}
{isNickname && !isEditing && (
<button
onClick={() => setIsEditing(true)}
type="button"
onClick={() => {
setIsEditing(true);
onEditStart?.();
}}
className="absolute right-3 top-1/2 -translate-y-1/2 font-body1 px-3 py-1 rounded-full bg-default-gray-400 text-default-gray-700"
{/* 수정 */}
{isNickname && !isEditing && (
<button
type="button"
disabled={readOnly}
onClick={() => {
if (readOnly) return;
setIsEditing(true);
onEditStart?.();
}}
className="absolute right-3 top-1/2 -translate-y-1/2 font-body1 px-3 py-1 rounded-full bg-default-gray-400 text-default-gray-700"
>
수정
</button>
)}
🤖 Prompt for AI Agents
In src/components/common/EditableInputBox.tsx around lines 118 to 126, the "수정"
button is still clickable when readOnly=true; update the button to respect
readOnly by adding the disabled attribute (disabled={readOnly}) and
aria-disabled for accessibility, and guard the onClick handler so it returns
early if readOnly is true (preventing setIsEditing and onEditStart from
running). Ensure styling reflects disabled state if needed and keep existing
className unchanged except to add any disabled styles.

>
수정
</button>
)}

{/* 글자 수 표시 */}
{/* 글자 수 */}
{isNickname && isEditing && (
<span className="absolute bottom-2 right-4 font-body1 text-default-gray-500">
{value.length} / {maxLength}
</span>
)}

{/* 검색 버튼 */}
{/* 검색 */}
{isSearch && (
<button type="button" onClick={onSearchClick} className="absolute right-3 top-1/2 -translate-y-1/2 p-1">
<SearchIcon className="w-5 h-5 text-primary-500" stroke="currentColor" />
</button>
)}
Comment on lines 140 to 144
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

검색 버튼에 접근성 라벨 추가 권장

아이콘 버튼은 스크린리더에 의미가 전달되지 않습니다. aria-label을 추가해 주세요.

-                {isSearch && (
-                    <button type="button" onClick={onSearchClick} className="absolute right-3 top-1/2 -translate-y-1/2 p-1">
+                {isSearch && (
+                    <button type="button" aria-label="검색 실행" onClick={onSearchClick} className="absolute right-3 top-1/2 -translate-y-1/2 p-1">
                         <SearchIcon className="w-5 h-5 text-primary-500" stroke="currentColor" />
                     </button>
                 )}
📝 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
{isSearch && (
<button type="button" onClick={onSearchClick} className="absolute right-3 top-1/2 -translate-y-1/2 p-1">
<SearchIcon className="w-5 h-5 text-primary-500" stroke="currentColor" />
</button>
)}
{isSearch && (
<button
type="button"
aria-label="검색 실행"
onClick={onSearchClick}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1"
>
<SearchIcon
className="w-5 h-5 text-primary-500"
stroke="currentColor"
/>
</button>
)}
🤖 Prompt for AI Agents
In src/components/common/EditableInputBox.tsx around lines 140 to 144, the
search icon button lacks an accessible name for screen readers; add an
aria-label (e.g., aria-label="Search") or aria-labelledby that provides a clear,
localizable description, or pass the label via props if needed, keeping the
existing button type and classes so the icon remains visually unchanged but is
announced by assistive technologies.

</div>

{/* 취소, 완료 버튼 */}
{/* 취소, 완료 */}
{isNickname && isEditing && (
<div className="flex justify-end gap-2 mt-3">
<button onClick={handleCancel} className="font-body1 px-4 py-1.5 rounded-full bg-default-gray-400 text-default-gray-700">
Expand Down
Loading