fix: [review] 공통 컴포넌트 규칙 위반 수정 (코드리뷰 반영)

- NotificationSettings/actions.ts: buildApiUrl() 패턴으로 전환
- NotificationSettings/types.ts: OrderItemVisibility에 approvalRequest 누락 추가
- NotificationSettings/index.tsx: 모듈 스코프 Audio → useRef 전환
- MasterFieldTab/index.tsx: 'use client' 선언 추가
- StatCards.tsx: 6개 이상 그리드 col-span 로직 수정
- ImportInspectionInputModal.tsx: 테스트입력 버튼 dev 환경 게이팅
- api/client.ts, api/index.ts: 422 에러 error.details 폴백 추가
This commit is contained in:
유병철
2026-03-19 10:52:42 +09:00
parent b25e7d53b6
commit 8d15c2391d
8 changed files with 55 additions and 34 deletions

View File

@@ -1,3 +1,5 @@
'use client';
/**
* 항목 탭 컴포넌트
*

View File

@@ -837,7 +837,7 @@ export function ImportInspectionInputModal({
<DialogHeader className="px-6 pt-6 pb-0 shrink-0">
<div className="flex items-center gap-2">
<DialogTitle className="text-lg font-bold"></DialogTitle>
{template && (
{process.env.NODE_ENV === 'development' && template && (
<button
type="button"
onClick={isTestFilled ? clearTestData : fillTestData}

View File

@@ -44,7 +44,7 @@ export function StatCards({ stats }: StatCardsProps) {
} ${
stat.isActive ? 'border-primary bg-primary/5' : ''
} ${
count % 2 === 1 && index === count - 1 ? 'col-span-2 sm:col-span-1' : ''
count % 2 === 1 && index === count - 1 && count < 6 ? 'col-span-2 sm:col-span-1' : ''
}`}
onClick={stat.onClick}
>

View File

@@ -2,17 +2,16 @@
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
import { buildApiUrl } from '@/lib/api/query-params';
import type { NotificationSettings } from './types';
import { DEFAULT_NOTIFICATION_SETTINGS } from './types';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== 알림 설정 조회 =====
export async function getNotificationSettings(): Promise<{
success: boolean; data: NotificationSettings; error?: string; __authError?: boolean;
}> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/settings/notifications`,
url: buildApiUrl('/api/v1/settings/notifications'),
transform: (data: Record<string, unknown>) => transformApiToFrontend(data),
errorMessage: '알림 설정 조회에 실패했습니다.',
});
@@ -30,7 +29,7 @@ export async function getNotificationSettings(): Promise<{
// ===== 알림 설정 저장 =====
export async function saveNotificationSettings(settings: NotificationSettings): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/settings/notifications`,
url: buildApiUrl('/api/v1/settings/notifications'),
method: 'PUT',
body: transformFrontendToApi(settings),
errorMessage: '알림 설정 저장에 실패했습니다.',

View File

@@ -7,7 +7,7 @@
* 항목 설정 기능으로 표시할 알림 카테고리/항목을 선택할 수 있습니다.
*/
import { useState, useCallback } from 'react';
import { useState, useCallback, useRef } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Bell, Save, Play, Settings } from 'lucide-react';
@@ -28,38 +28,16 @@ import { SOUND_OPTIONS, DEFAULT_ITEM_VISIBILITY } from './types';
import { saveNotificationSettings } from './actions';
import { ItemSettingsDialog } from './ItemSettingsDialog';
// 미리듣기 - Audio API 재생
let previewAudio: HTMLAudioElement | null = null;
function playPreviewSound(soundType: SoundType) {
if (soundType === 'mute') {
toast.info('무음으로 설정되어 있습니다.');
return;
}
// 이전 재생 중지
if (previewAudio) {
previewAudio.pause();
previewAudio = null;
}
const soundFile = soundType === 'sam_voice' ? 'sam_voice.wav' : 'default.wav';
previewAudio = new Audio(`/sounds/${soundFile}`);
previewAudio.play().catch(() => {
const soundName = soundType === 'default' ? '기본 알림음' : 'SAM 보이스';
toast.info(`${soundName} 미리듣기 (음원 파일 준비 중)`);
});
}
// 알림 항목 컴포넌트
interface NotificationItemRowProps {
label: string;
item: NotificationItem;
onChange: (item: NotificationItem) => void;
onPlayPreview: (soundType: SoundType) => void;
disabled?: boolean;
}
function NotificationItemRow({ label, item, onChange, disabled }: NotificationItemRowProps) {
function NotificationItemRow({ label, item, onChange, onPlayPreview, disabled }: NotificationItemRowProps) {
const isDisabled = disabled || !item.enabled;
return (
@@ -107,7 +85,7 @@ function NotificationItemRow({ label, item, onChange, disabled }: NotificationIt
variant="outline"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => playPreviewSound(item.soundType)}
onClick={() => onPlayPreview(item.soundType)}
disabled={isDisabled}
>
<Play className="h-3 w-3" />
@@ -190,6 +168,28 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
toast.success('항목 설정이 저장되었습니다.');
}, []);
// 미리듣기 - Audio API 재생 (useRef로 메모리 누수 방지)
const previewAudioRef = useRef<HTMLAudioElement | null>(null);
const handlePlayPreview = useCallback((soundType: SoundType) => {
if (soundType === 'mute') {
toast.info('무음으로 설정되어 있습니다.');
return;
}
if (previewAudioRef.current) {
previewAudioRef.current.pause();
previewAudioRef.current = null;
}
const soundFile = soundType === 'sam_voice' ? 'sam_voice.wav' : 'default.wav';
previewAudioRef.current = new Audio(`/sounds/${soundFile}`);
previewAudioRef.current.play().catch(() => {
const soundName = soundType === 'default' ? '기본 알림음' : 'SAM 보이스';
toast.info(`${soundName} 미리듣기 (음원 파일 준비 중)`);
});
}, []);
// 공지 알림 핸들러
const handleNoticeEnabledChange = (enabled: boolean) => {
setSettings(prev => ({
@@ -405,6 +405,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="공지사항 알림"
item={settings.notice.notice}
onChange={(item) => handleNoticeItemChange('notice', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.notice.enabled}
/>
)}
@@ -413,6 +414,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="이벤트 알림"
item={settings.notice.event}
onChange={(item) => handleNoticeItemChange('event', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.notice.enabled}
/>
)}
@@ -431,6 +433,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="부가세 신고 알림"
item={settings.schedule.vatReport}
onChange={(item) => handleScheduleItemChange('vatReport', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.schedule.enabled}
/>
)}
@@ -439,6 +442,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="종합소득세 신고 알림"
item={settings.schedule.incomeTaxReport}
onChange={(item) => handleScheduleItemChange('incomeTaxReport', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.schedule.enabled}
/>
)}
@@ -457,6 +461,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="신규 업체 등록 알림"
item={settings.vendor.newVendor}
onChange={(item) => handleVendorItemChange('newVendor', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.vendor.enabled}
/>
)}
@@ -465,6 +470,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="신용등급 등록 알림"
item={settings.vendor.creditRating}
onChange={(item) => handleVendorItemChange('creditRating', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.vendor.enabled}
/>
)}
@@ -483,6 +489,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="연차 알림"
item={settings.attendance.annualLeave}
onChange={(item) => handleAttendanceItemChange('annualLeave', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.attendance.enabled}
/>
)}
@@ -491,6 +498,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="출근 알림"
item={settings.attendance.clockIn}
onChange={(item) => handleAttendanceItemChange('clockIn', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.attendance.enabled}
/>
)}
@@ -499,6 +507,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="지각 알림"
item={settings.attendance.late}
onChange={(item) => handleAttendanceItemChange('late', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.attendance.enabled}
/>
)}
@@ -507,6 +516,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="결근 알림"
item={settings.attendance.absent}
onChange={(item) => handleAttendanceItemChange('absent', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.attendance.enabled}
/>
)}
@@ -525,6 +535,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="수주 등록 알림"
item={settings.order.salesOrder}
onChange={(item) => handleOrderItemChange('salesOrder', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.order.enabled}
/>
)}
@@ -533,6 +544,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="발주 알림"
item={settings.order.purchaseOrder}
onChange={(item) => handleOrderItemChange('purchaseOrder', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.order.enabled}
/>
)}
@@ -551,6 +563,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="결재요청 알림"
item={settings.approval.approvalRequest}
onChange={(item) => handleApprovalItemChange('approvalRequest', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.approval.enabled}
/>
)}
@@ -559,6 +572,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="기안 > 승인 알림"
item={settings.approval.draftApproved}
onChange={(item) => handleApprovalItemChange('draftApproved', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.approval.enabled}
/>
)}
@@ -567,6 +581,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="기안 > 반려 알림"
item={settings.approval.draftRejected}
onChange={(item) => handleApprovalItemChange('draftRejected', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.approval.enabled}
/>
)}
@@ -575,6 +590,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="기안 > 완료 알림"
item={settings.approval.draftCompleted}
onChange={(item) => handleApprovalItemChange('draftCompleted', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.approval.enabled}
/>
)}
@@ -593,6 +609,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="안전재고 알림"
item={settings.production.safetyStock}
onChange={(item) => handleProductionItemChange('safetyStock', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.production.enabled}
/>
)}
@@ -601,6 +618,7 @@ export function NotificationSettingsManagement({ initialData }: NotificationSett
label="생산완료 알림"
item={settings.production.productionComplete}
onChange={(item) => handleProductionItemChange('productionComplete', item)}
onPlayPreview={handlePlayPreview}
disabled={!settings.production.enabled}
/>
)}

View File

@@ -125,6 +125,7 @@ export interface OrderItemVisibility {
enabled: boolean;
salesOrder: boolean; // 수주 알림
purchaseOrder: boolean; // 발주 알림
approvalRequest: boolean; // 결재요청 알림
}
// 전자결재 알림 항목 설정
@@ -232,6 +233,7 @@ export const DEFAULT_ITEM_VISIBILITY: ItemVisibilitySettings = {
enabled: true,
salesOrder: true,
purchaseOrder: true,
approvalRequest: true,
},
approval: {
enabled: true,

View File

@@ -178,7 +178,7 @@ export class ApiClient {
const error: ApiErrorResponse = {
message: data.message || 'An error occurred',
errors: data.errors,
errors: data.errors || data.error?.details,
code: data.code,
};

View File

@@ -174,7 +174,7 @@ class ServerApiClient {
throw {
status: response.status,
message: errorData.message || 'An error occurred',
errors: errorData.errors,
errors: errorData.errors || errorData.error?.details,
code: errorData.code,
};
}