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:
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 항목 탭 컴포넌트
|
||||
*
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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: '알림 설정 저장에 실패했습니다.',
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user