fix(WEB): 토큰 만료 시 무한 로딩 대신 로그인 리다이렉트 처리

- 52개 이상의 컴포넌트에 isNextRedirectError 처리 추가
- Server Action의 redirect() 에러가 catch 블록에서 삼켜지는 문제 해결
- access_token + refresh_token 모두 만료 시 정상적으로 로그인 페이지로 리다이렉트

수정된 영역:
- accounting: 10개 컴포넌트
- production: 12개 컴포넌트
- hr: 5개 컴포넌트
- settings: 8개 컴포넌트
- approval: 5개 컴포넌트
- items: 20개+ 컴포넌트
- board: 5개 컴포넌트
- quality: 4개 컴포넌트
- material, outbound, quotes 등 기타 컴포넌트

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-11 17:19:11 +09:00
parent 8bc4b90fe9
commit e56b7d53a4
131 changed files with 3320 additions and 1979 deletions

View File

@@ -7,7 +7,7 @@
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { BoardDetail } from '@/components/board/BoardDetail';
import { getPost } from '@/components/board/actions';
import type { Post, Comment } from '@/components/board/types';
@@ -60,11 +60,7 @@ export default function BoardDetailPage() {
}, [boardCode, postId, router]);
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
</div>
);
return <ContentLoadingSpinner text="게시글을 불러오는 중..." />;
}
if (!post) {

View File

@@ -3,6 +3,7 @@
import { useRouter, useParams } from 'next/navigation';
import { useState, useEffect, useCallback } from 'react';
import { Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { BoardForm } from '@/components/board/BoardManagement/BoardForm';
import { getBoardById, updateBoard } from '@/components/board/BoardManagement/actions';
import { forceRefreshMenus } from '@/lib/utils/menuRefresh';
@@ -64,11 +65,7 @@ export default function BoardEditPage() {
// 로딩 상태
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div>
);
return <ContentLoadingSpinner text="게시판 정보를 불러오는 중..." />;
}
// 에러 상태

View File

@@ -3,6 +3,7 @@
import { useRouter, useParams } from 'next/navigation';
import { useState, useEffect, useCallback } from 'react';
import { Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { BoardDetail } from '@/components/board/BoardManagement/BoardDetail';
import { getBoardById, deleteBoard } from '@/components/board/BoardManagement/actions';
import { Button } from '@/components/ui/button';
@@ -74,11 +75,7 @@ export default function BoardDetailPage() {
// 로딩 상태
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div>
);
return <ContentLoadingSpinner text="게시판 정보를 불러오는 중..." />;
}
// 에러 상태

View File

@@ -6,7 +6,8 @@
import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { ArrowLeft, Save, MessageSquare, Loader2 } from 'lucide-react';
import { ArrowLeft, Save, MessageSquare } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -146,9 +147,7 @@ export default function DynamicBoardEditPage() {
if (isLoading) {
return (
<PageLayout>
<div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
<ContentLoadingSpinner text="게시글을 불러오는 중..." />
</PageLayout>
);
}

View File

@@ -13,7 +13,7 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation';
import DynamicItemForm from '@/components/items/DynamicItemForm';
import type { DynamicFormData, BOMLine } from '@/components/items/DynamicItemForm/types';
import type { ItemType } from '@/types/item';
import { Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import {
isMaterialType,
transformMaterialDataForSave,
@@ -391,12 +391,7 @@ export default function EditItemPage() {
// 로딩 상태
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center py-12 gap-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground"> ...</p>
</div>
);
return <ContentLoadingSpinner text="품목 정보를 불러오는 중..." />;
}
// 에러 상태

View File

@@ -11,7 +11,7 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { notFound } from 'next/navigation';
import ItemDetailClient from '@/components/items/ItemDetailClient';
import type { ItemMaster, ItemType, ProductCategory, PartType, PartUsage } from '@/types/item';
import { Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
// Materials 타입 (SM, RM, CS는 Material 테이블 사용)
const MATERIAL_TYPES = ['SM', 'RM', 'CS'];
@@ -255,12 +255,7 @@ export default function ItemDetailPage() {
// 로딩 상태
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center py-12 gap-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground"> ...</p>
</div>
);
return <ContentLoadingSpinner text="품목 정보를 불러오는 중..." />;
}
// 에러 상태

View File

@@ -6,10 +6,11 @@
import { Suspense } from 'react';
import ProductionDashboard from '@/components/production/ProductionDashboard';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
export default function ProductionDashboardPage() {
return (
<Suspense fallback={<div className="text-center py-8"> ...</div>}>
<Suspense fallback={<ContentLoadingSpinner text="생산 현황을 불러오는 중..." />}>
<ProductionDashboard />
</Suspense>
);

View File

@@ -6,6 +6,7 @@
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import ItemForm from '@/components/items/ItemForm';
import type { ItemMaster } from '@/types/item';
import type { CreateItemFormData } from '@/lib/utils/validation';
@@ -189,11 +190,7 @@ export default function EditItemPage() {
};
if (isLoading) {
return (
<div className="p-6">
<div className="text-center py-8"> ...</div>
</div>
);
return <ContentLoadingSpinner text="품목 정보를 불러오는 중..." />;
}
if (!item) {

View File

@@ -6,6 +6,7 @@
import { use, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import ItemDetailClient from '@/components/items/ItemDetailClient';
import type { ItemMaster } from '@/types/item';
@@ -159,11 +160,7 @@ export default function ItemDetailPage({
}, [id]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-muted-foreground"> ...</div>
</div>
);
return <ContentLoadingSpinner text="품목 정보를 불러오는 중..." />;
}
if (!item) {

View File

@@ -6,10 +6,11 @@
import { Suspense } from 'react';
import WorkerScreen from '@/components/production/WorkerScreen';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
export default function WorkerScreenPage() {
return (
<Suspense fallback={<div className="text-center py-8"> ...</div>}>
<Suspense fallback={<ContentLoadingSpinner text="작업자 화면을 불러오는 중..." />}>
<WorkerScreen />
</Suspense>
);

View File

@@ -97,17 +97,17 @@ export function Day1ChecklistPanel({
return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden h-full flex flex-col">
{/* 헤더 + 검색 */}
<div className="bg-gray-100 px-4 py-3 border-b border-gray-200">
<h3 className="font-semibold text-gray-900 mb-2"> </h3>
<div className="bg-gray-100 px-2 sm:px-4 py-2 sm:py-3 border-b border-gray-200">
<h3 className="font-semibold text-gray-900 text-sm sm:text-base mb-2"> </h3>
{/* 검색 입력 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<Search className="absolute left-2 sm:left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="항목 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-9 pr-8 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="w-full pl-8 sm:pl-9 pr-8 py-1.5 sm:py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
{searchTerm && (
<button
@@ -121,7 +121,7 @@ export function Day1ChecklistPanel({
</div>
{/* 검색 결과 카운트 */}
{searchTerm && (
<div className="mt-2 text-xs text-gray-500">
<div className="mt-1.5 sm:mt-2 text-xs text-gray-500">
{filteredCategories.length > 0
? `${filteredCategories.reduce((sum, cat) => sum + cat.subItems.length, 0)}개 항목 검색됨`
: '검색 결과가 없습니다'
@@ -151,7 +151,7 @@ export function Day1ChecklistPanel({
type="button"
onClick={() => toggleCategory(category.id)}
className={cn(
'w-full flex items-center justify-between px-4 py-3 text-left transition-colors',
'w-full flex items-center justify-between px-2 sm:px-4 py-2 sm:py-3 text-left transition-colors',
'hover:bg-gray-50',
allCompleted && 'bg-green-50'
)}
@@ -231,7 +231,7 @@ function SubItemRow({
return (
<div
className={cn(
'flex items-center gap-3 px-4 py-2.5 cursor-pointer transition-colors',
'flex items-center gap-2 sm:gap-3 px-2 sm:px-4 py-2 sm:py-2.5 cursor-pointer transition-colors',
isSelected
? 'bg-blue-100 border-l-4 border-blue-500'
: 'hover:bg-gray-100 border-l-4 border-transparent',

View File

@@ -35,22 +35,22 @@ export function Day1DocumentSection({
return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden h-full flex flex-col">
{/* 헤더 */}
<div className="bg-gray-100 px-4 py-3 border-b border-gray-200">
<h3 className="font-semibold text-gray-900"> </h3>
<div className="bg-gray-100 px-2 sm:px-4 py-2 sm:py-3 border-b border-gray-200">
<h3 className="font-semibold text-gray-900 text-sm sm:text-base"> </h3>
</div>
{/* 콘텐츠 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<div className="flex-1 overflow-y-auto p-3 sm:p-4 space-y-3 sm:space-y-4">
{/* 항목 정보 */}
<div className="bg-blue-50 rounded-lg p-4">
<h4 className="font-medium text-blue-900 mb-2">{checkItem.title}</h4>
<p className="text-sm text-blue-700">{checkItem.description}</p>
<div className="bg-blue-50 rounded-lg p-3 sm:p-4">
<h4 className="font-medium text-blue-900 mb-1 sm:mb-2 text-sm sm:text-base">{checkItem.title}</h4>
<p className="text-xs sm:text-sm text-blue-700">{checkItem.description}</p>
</div>
{/* 기준 문서 목록 */}
<div>
<h5 className="text-sm font-medium text-gray-700 mb-2"> </h5>
<div className="space-y-2">
<h5 className="text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2"> </h5>
<div className="space-y-1.5 sm:space-y-2">
{checkItem.standardDocuments.map((doc) => (
<DocumentRow
key={doc.id}
@@ -63,7 +63,7 @@ export function Day1DocumentSection({
</div>
{/* 확인 버튼 */}
<div className="pt-4 border-t border-gray-200">
<div className="pt-3 sm:pt-4 border-t border-gray-200">
<Button
onClick={onConfirmComplete}
disabled={isCompleted}
@@ -101,7 +101,7 @@ function DocumentRow({ document, isSelected, onSelect }: DocumentRowProps) {
return (
<div
className={cn(
'flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors',
'flex items-center gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border cursor-pointer transition-colors',
isSelected
? 'bg-blue-50 border-blue-300'
: 'bg-gray-50 border-gray-200 hover:bg-gray-100'
@@ -110,12 +110,12 @@ function DocumentRow({ document, isSelected, onSelect }: DocumentRowProps) {
>
{/* 아이콘 */}
<div className={cn(
'flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center',
'flex-shrink-0 w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center',
document.fileName?.endsWith('.pdf')
? 'bg-red-100 text-red-600'
: 'bg-green-100 text-green-600'
)}>
<FileText className="h-5 w-5" />
<FileText className="h-4 w-4 sm:h-5 sm:w-5" />
</div>
{/* 문서 정보 */}

View File

@@ -27,19 +27,19 @@ export function Day1DocumentViewer({ document }: Day1DocumentViewerProps) {
return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden h-full flex flex-col">
{/* 헤더 */}
<div className="bg-gray-100 px-4 py-3 border-b border-gray-200 flex items-center justify-between">
<div className="bg-gray-100 px-2 sm:px-4 py-2 sm:py-3 border-b border-gray-200 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={cn(
'w-8 h-8 rounded flex items-center justify-center',
'w-6 h-6 sm:w-8 sm:h-8 rounded flex items-center justify-center',
isPdf ? 'bg-red-100 text-red-600' :
isExcel ? 'bg-green-100 text-green-600' :
'bg-gray-200 text-gray-600'
)}>
<FileText className="h-4 w-4" />
<FileText className="h-3 w-3 sm:h-4 sm:w-4" />
</div>
<div>
<h3 className="font-medium text-gray-900 text-sm">{document.title}</h3>
<p className="text-xs text-gray-500">
<h3 className="font-medium text-gray-900 text-xs sm:text-sm">{document.title}</h3>
<p className="text-[10px] sm:text-xs text-gray-500">
{document.version !== '-' && <span className="mr-2">{document.version}</span>}
{document.date}
</p>
@@ -47,60 +47,60 @@ export function Day1DocumentViewer({ document }: Day1DocumentViewerProps) {
</div>
{/* 툴바 */}
<div className="flex items-center gap-1">
<div className="flex items-center gap-0.5 sm:gap-1">
<button
type="button"
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
className="p-1.5 sm:p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
title="축소"
>
<ZoomOut className="h-4 w-4" />
<ZoomOut className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</button>
<button
type="button"
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
className="p-1.5 sm:p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
title="확대"
>
<ZoomIn className="h-4 w-4" />
<ZoomIn className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</button>
<button
type="button"
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
className="hidden sm:block p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
title="전체화면"
>
<Maximize2 className="h-4 w-4" />
</button>
<div className="w-px h-6 bg-gray-300 mx-1" />
<div className="hidden sm:block w-px h-6 bg-gray-300 mx-1" />
<button
type="button"
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
className="hidden sm:block p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
title="인쇄"
>
<Printer className="h-4 w-4" />
</button>
<button
type="button"
className="p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
className="p-1.5 sm:p-2 rounded hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
title="다운로드"
>
<Download className="h-4 w-4" />
<Download className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</button>
</div>
</div>
{/* 문서 미리보기 영역 */}
<div className="flex-1 bg-gray-200 p-4 overflow-auto">
<div className="bg-white rounded shadow-lg max-w-3xl mx-auto min-h-[600px]">
<div className="flex-1 bg-gray-200 p-2 sm:p-4 overflow-auto">
<div className="bg-white rounded shadow-lg max-w-3xl mx-auto min-h-[400px] sm:min-h-[600px]">
{/* Mock 문서 내용 */}
<DocumentPreviewContent document={document} />
</div>
</div>
{/* 푸터 */}
<div className="bg-gray-100 px-4 py-2 border-t border-gray-200 flex items-center justify-between">
<span className="text-xs text-gray-500">
<div className="bg-gray-100 px-2 sm:px-4 py-1.5 sm:py-2 border-t border-gray-200 flex items-center justify-between">
<span className="text-[10px] sm:text-xs text-gray-500 truncate max-w-[60%]">
: {document.fileName || '-'}
</span>
<span className="text-xs text-gray-500">
<span className="text-[10px] sm:text-xs text-gray-500">
1 / 1
</span>
</div>

View File

@@ -25,24 +25,27 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
: 0;
return (
<div className="mb-4 space-y-3">
<div className="mb-3 md:mb-4 space-y-2 md:space-y-3">
{/* 탭 버튼 */}
<div className="flex gap-3">
<div className="flex gap-2 md:gap-3">
{/* 1일차 탭 */}
<button
type="button"
onClick={() => onDayChange(1)}
className={cn(
'flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-lg border-2 transition-all',
'flex-1 flex items-center justify-center gap-1 sm:gap-2 py-2 sm:py-3 px-2 sm:px-4 rounded-lg border-2 transition-all',
activeDay === 1
? 'bg-blue-600 border-blue-600 text-white shadow-lg'
: 'bg-white border-gray-200 text-gray-700 hover:border-blue-300 hover:bg-blue-50'
)}
>
<Calendar className="h-4 w-4" />
<span className="font-medium">1일차: 기준/ </span>
<Calendar className="h-4 w-4 shrink-0" />
<span className="font-medium text-xs sm:text-sm">
<span className="hidden sm:inline">1일차: 기준/</span>
<span className="sm:hidden">1</span>
</span>
<span className={cn(
'text-xs px-2 py-0.5 rounded-full ml-2',
'text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 rounded-full ml-1 sm:ml-2 shrink-0',
activeDay === 1 ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-600'
)}>
{day1Progress.completed}/{day1Progress.total}
@@ -54,16 +57,19 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
type="button"
onClick={() => onDayChange(2)}
className={cn(
'flex-1 flex items-center justify-center gap-2 py-3 px-4 rounded-lg border-2 transition-all',
'flex-1 flex items-center justify-center gap-1 sm:gap-2 py-2 sm:py-3 px-2 sm:px-4 rounded-lg border-2 transition-all',
activeDay === 2
? 'bg-blue-600 border-blue-600 text-white shadow-lg'
: 'bg-white border-gray-200 text-gray-700 hover:border-blue-300 hover:bg-blue-50'
)}
>
<Calendar className="h-4 w-4" />
<span className="font-medium">2일차: 로트추적 </span>
<Calendar className="h-4 w-4 shrink-0" />
<span className="font-medium text-xs sm:text-sm">
<span className="hidden sm:inline">2일차: 로트추적</span>
<span className="sm:hidden">2</span>
</span>
<span className={cn(
'text-xs px-2 py-0.5 rounded-full ml-2',
'text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 rounded-full ml-1 sm:ml-2 shrink-0',
activeDay === 2 ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-600'
)}>
{day2Progress.completed}/{day2Progress.total}
@@ -72,11 +78,14 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
</div>
{/* 진행률 - 3줄 표시 */}
<div className="bg-white rounded-lg border border-gray-200 px-4 py-3 space-y-2">
<div className="bg-white rounded-lg border border-gray-200 px-2 sm:px-4 py-2 sm:py-3 space-y-1.5 sm:space-y-2">
{/* 전체 심사 진행률 */}
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-gray-700 w-28 shrink-0"> </span>
<div className="flex-1 h-2.5 bg-gray-200 rounded-full overflow-hidden">
<div className="flex items-center gap-2 sm:gap-3">
<span className="text-xs sm:text-sm font-medium text-gray-700 w-14 sm:w-28 shrink-0">
<span className="hidden sm:inline"> </span>
<span className="sm:hidden"></span>
</span>
<div className="flex-1 h-2 sm:h-2.5 bg-gray-200 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all duration-500',
@@ -86,7 +95,7 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
/>
</div>
<span className={cn(
'text-sm font-bold w-16 text-right',
'text-xs sm:text-sm font-bold w-12 sm:w-16 text-right shrink-0',
overallPercentage === 100 ? 'text-green-600' : 'text-blue-600'
)}>
{totalCompleted}/{totalItems}
@@ -94,9 +103,12 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
</div>
{/* 1일차 진행률 */}
<div className="flex items-center gap-3">
<span className="text-sm text-gray-600 w-28 shrink-0">1일차: 기준/</span>
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="flex items-center gap-2 sm:gap-3">
<span className="text-xs sm:text-sm text-gray-600 w-14 sm:w-28 shrink-0">
<span className="hidden sm:inline">1일차: 기준/</span>
<span className="sm:hidden">1</span>
</span>
<div className="flex-1 h-1.5 sm:h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all duration-500',
@@ -106,7 +118,7 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
/>
</div>
<span className={cn(
'text-sm font-medium w-16 text-right',
'text-xs sm:text-sm font-medium w-12 sm:w-16 text-right shrink-0',
day1Percentage === 100 ? 'text-green-600' : 'text-gray-600'
)}>
{day1Progress.completed}/{day1Progress.total}
@@ -114,9 +126,12 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
</div>
{/* 2일차 진행률 */}
<div className="flex items-center gap-3">
<span className="text-sm text-gray-600 w-28 shrink-0">2일차: 로트추적</span>
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="flex items-center gap-2 sm:gap-3">
<span className="text-xs sm:text-sm text-gray-600 w-14 sm:w-28 shrink-0">
<span className="hidden sm:inline">2일차: 로트추적</span>
<span className="sm:hidden">2</span>
</span>
<div className="flex-1 h-1.5 sm:h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all duration-500',
@@ -126,7 +141,7 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }:
/>
</div>
<span className={cn(
'text-sm font-medium w-16 text-right',
'text-xs sm:text-sm font-medium w-12 sm:w-16 text-right shrink-0',
day2Percentage === 100 ? 'text-green-600' : 'text-gray-600'
)}>
{day2Progress.completed}/{day2Progress.total}

View File

@@ -51,15 +51,15 @@ export const DocumentList = ({ documents, routeCode, onViewDocument }: DocumentL
};
return (
<div className="bg-white rounded-lg p-4 shadow-sm h-full flex flex-col overflow-hidden">
<h2 className="font-bold text-gray-800 text-sm mb-4">
<div className="bg-white rounded-lg p-3 sm:p-4 shadow-sm h-full flex flex-col overflow-hidden">
<h2 className="font-bold text-gray-800 text-xs sm:text-sm mb-3 sm:mb-4">
{' '}
{routeCode && (
<span className="text-gray-400 font-normal ml-1">({routeCode})</span>
)}
</h2>
<div className="space-y-3 overflow-y-auto flex-1">
<div className="space-y-2 sm:space-y-3 overflow-y-auto flex-1">
{!routeCode ? (
<div className="flex items-center justify-center h-32 text-gray-400 text-sm">
.
@@ -74,7 +74,7 @@ export const DocumentList = ({ documents, routeCode, onViewDocument }: DocumentL
<div key={doc.id} className="border border-gray-200 rounded-lg overflow-hidden">
<div
onClick={() => handleDocClick(doc)}
className={`p-4 flex justify-between items-center transition-colors ${
className={`p-3 sm:p-4 flex justify-between items-center transition-colors ${
hasItems ? 'cursor-pointer hover:bg-gray-50' : 'cursor-default opacity-60'
} ${isExpanded ? 'bg-green-50' : 'bg-white'}`}
>
@@ -99,13 +99,13 @@ export const DocumentList = ({ documents, routeCode, onViewDocument }: DocumentL
</div>
{isExpanded && hasMultipleItems && (
<div className="bg-white px-4 pb-4 space-y-2">
<div className="h-px bg-gray-100 w-full mb-3" />
<div className="bg-white px-3 sm:px-4 pb-3 sm:pb-4 space-y-1.5 sm:space-y-2">
<div className="h-px bg-gray-100 w-full mb-2 sm:mb-3" />
{doc.items!.map((item) => (
<div
key={item.id}
onClick={() => handleItemClick(doc, item)}
className="flex items-center justify-between border border-gray-100 p-3 rounded cursor-pointer hover:bg-green-50 hover:border-green-200 transition-colors group"
className="flex items-center justify-between border border-gray-100 p-2 sm:p-3 rounded cursor-pointer hover:bg-green-50 hover:border-green-200 transition-colors group"
>
<div>
<div className="text-xs font-bold text-gray-700">{item.title}</div>

View File

@@ -24,13 +24,13 @@ export const Filters = ({
const years = [2025, 2024, 2023, 2022, 2021];
return (
<div className="w-full bg-white p-4 rounded-lg mb-4 shadow-sm">
<div className="w-full bg-white p-3 sm:p-4 rounded-lg mb-3 sm:mb-4 shadow-sm">
{/* 상단: 년도/분기 선택 */}
<div className="flex flex-wrap items-end gap-4 mb-4">
<div className="flex flex-wrap items-end gap-3 sm:gap-4 mb-3 sm:mb-4">
{/* Year Selection */}
<div className="flex flex-col gap-1">
<span className="text-xs font-semibold text-gray-500"></span>
<div className="w-32">
<div className="w-28 sm:w-32">
<select
value={selectedYear}
onChange={(e) => onYearChange(parseInt(e.target.value))}
@@ -78,7 +78,7 @@ export const Filters = ({
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md text-sm focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
/>
</div>
<button className="bg-[#1e3a8a] text-white px-6 py-2 rounded-md text-sm hover:bg-blue-800 transition-colors whitespace-nowrap">
<button className="bg-[#1e3a8a] text-white px-4 sm:px-6 py-2 rounded-md text-sm hover:bg-blue-800 transition-colors whitespace-nowrap">
</button>
</div>

View File

@@ -6,10 +6,10 @@ interface HeaderProps {
export const Header = ({ rightContent }: HeaderProps) => {
return (
<div className="w-full bg-[#1e3a8a] text-white p-6 rounded-lg mb-4 shadow-md flex items-center justify-between h-24">
<div className="w-full bg-[#1e3a8a] text-white p-3 sm:p-6 rounded-lg mb-3 sm:mb-4 shadow-md flex items-center justify-between h-16 sm:h-24">
<div className="flex flex-col justify-center">
<h1 className="text-2xl font-bold mb-1"> </h1>
<p className="text-sm opacity-80 text-blue-100">SAM - Smart Automation Management</p>
<h1 className="text-lg sm:text-2xl font-bold mb-0.5 sm:mb-1"> </h1>
<p className="text-xs sm:text-sm opacity-80 text-blue-100">SAM - Smart Automation Management</p>
</div>
{rightContent && (
<div className="flex items-center">

View File

@@ -12,15 +12,15 @@ interface ReportListProps {
export const ReportList = ({ reports, selectedId, onSelect }: ReportListProps) => {
return (
<div className="bg-white rounded-lg p-4 shadow-sm h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<h2 className="font-bold text-lg text-gray-800"> </h2>
<span className="bg-blue-100 text-blue-800 text-xs font-bold px-2 py-1 rounded-full">
<div className="bg-white rounded-lg p-3 sm:p-4 shadow-sm h-full flex flex-col">
<div className="flex items-center justify-between mb-3 sm:mb-4">
<h2 className="font-bold text-sm sm:text-lg text-gray-800"> </h2>
<span className="bg-blue-100 text-blue-800 text-[10px] sm:text-xs font-bold px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-full">
{reports.length}
</span>
</div>
<div className="space-y-3 overflow-y-auto flex-1">
<div className="space-y-2 sm:space-y-3 overflow-y-auto flex-1">
{reports.length === 0 ? (
<div className="flex items-center justify-center h-32 text-gray-400 text-sm">
.
@@ -32,23 +32,23 @@ export const ReportList = ({ reports, selectedId, onSelect }: ReportListProps) =
<div
key={report.id}
onClick={() => onSelect(report)}
className={`rounded-lg p-4 cursor-pointer relative hover:shadow-md transition-all ${
className={`rounded-lg p-3 sm:p-4 cursor-pointer relative hover:shadow-md transition-all ${
isSelected
? 'border-2 border-blue-500 bg-blue-50'
: 'border border-gray-200 bg-white hover:border-blue-300'
}`}
>
<div className="absolute top-4 right-4 text-xs text-gray-400 bg-gray-100 px-2 py-1 rounded">
<div className="absolute top-3 sm:top-4 right-3 sm:right-4 text-[10px] sm:text-xs text-gray-400 bg-gray-100 px-1.5 sm:px-2 py-0.5 sm:py-1 rounded">
{report.quarter}
</div>
<h3 className={`font-bold text-lg mb-1 ${isSelected ? 'text-blue-900' : 'text-gray-800'}`}>
<h3 className={`font-bold text-sm sm:text-lg mb-1 ${isSelected ? 'text-blue-900' : 'text-gray-800'}`}>
{report.code}
</h3>
<p className="text-gray-700 font-medium mb-1">{report.siteName}</p>
<p className="text-sm text-gray-500 mb-3">: {report.item}</p>
<p className="text-xs sm:text-base text-gray-700 font-medium mb-1">{report.siteName}</p>
<p className="text-xs sm:text-sm text-gray-500 mb-2 sm:mb-3">: {report.item}</p>
<div className={`flex items-center gap-2 p-2 rounded text-sm font-medium ${
<div className={`flex items-center gap-1.5 sm:gap-2 p-1.5 sm:p-2 rounded text-xs sm:text-sm font-medium ${
isSelected ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'
}`}>
<Package size={16} />

View File

@@ -27,15 +27,15 @@ export const RouteList = ({ routes, selectedId, onSelect, onToggleItem, reportCo
};
return (
<div className="bg-white rounded-lg p-4 shadow-sm h-full flex flex-col overflow-hidden">
<h2 className="font-bold text-gray-800 text-sm mb-4">
<div className="bg-white rounded-lg p-3 sm:p-4 shadow-sm h-full flex flex-col overflow-hidden">
<h2 className="font-bold text-gray-800 text-xs sm:text-sm mb-3 sm:mb-4">
{' '}
{reportCode && (
<span className="text-gray-400 font-normal ml-1">({reportCode})</span>
)}
</h2>
<div className="space-y-3 overflow-y-auto flex-1">
<div className="space-y-2 sm:space-y-3 overflow-y-auto flex-1">
{routes.length === 0 ? (
<div className="flex items-center justify-center h-32 text-gray-400 text-sm">
{reportCode ? '수주루트가 없습니다.' : '품질관리서를 선택해주세요.'}
@@ -51,7 +51,7 @@ export const RouteList = ({ routes, selectedId, onSelect, onToggleItem, reportCo
<div key={route.id} className="border border-gray-200 rounded-lg overflow-hidden">
<div
onClick={() => handleClick(route)}
className={`p-4 cursor-pointer flex justify-between items-start transition-colors ${
className={`p-3 sm:p-4 cursor-pointer flex justify-between items-start transition-colors ${
isSelected ? 'bg-green-50 border-b border-green-100' : 'bg-white hover:bg-gray-50'
}`}
>
@@ -87,8 +87,8 @@ export const RouteList = ({ routes, selectedId, onSelect, onToggleItem, reportCo
</div>
{isExpanded && route.subItems.length > 0 && (
<div className="bg-white p-3 space-y-2">
<div className="text-xs font-bold text-gray-600 mb-2 flex items-center gap-1">
<div className="bg-white p-2 sm:p-3 space-y-1.5 sm:space-y-2">
<div className="text-xs font-bold text-gray-600 mb-1.5 sm:mb-2 flex items-center gap-1">
<MapPin size={10} />
</div>
{route.subItems.map((item) => (

View File

@@ -228,7 +228,7 @@ export default function QualityInspectionPage() {
}, []);
return (
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-6 bg-slate-100 flex flex-col lg:overflow-hidden">
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-3 sm:p-4 md:p-5 lg:p-6 bg-slate-100 flex flex-col lg:overflow-hidden">
{/* 헤더 (설정 버튼 포함) */}
<Header
rightContent={<SettingsButton onClick={() => setSettingsOpen(true)} />}
@@ -272,9 +272,9 @@ export default function QualityInspectionPage() {
{activeDay === 1 ? (
// ===== 1일차: 기준/매뉴얼 심사 =====
<div className="flex-1 grid grid-cols-12 gap-4 lg:min-h-0">
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:min-h-0">
{/* 좌측: 점검표 항목 */}
<div className={`col-span-12 min-h-[300px] lg:min-h-0 lg:h-full overflow-auto ${
<div className={`col-span-12 min-h-[250px] sm:min-h-[300px] lg:min-h-0 lg:h-full overflow-auto ${
displaySettings.showDocumentSection && displaySettings.showDocumentViewer
? 'lg:col-span-3'
: displaySettings.showDocumentSection || displaySettings.showDocumentViewer
@@ -291,7 +291,7 @@ export default function QualityInspectionPage() {
{/* 중앙: 기준 문서화 */}
{displaySettings.showDocumentSection && (
<div className={`col-span-12 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
displaySettings.showDocumentViewer ? 'lg:col-span-4' : 'lg:col-span-8'
}`}>
<Day1DocumentSection
@@ -306,7 +306,7 @@ export default function QualityInspectionPage() {
{/* 우측: 문서 뷰어 */}
{displaySettings.showDocumentViewer && (
<div className={`col-span-12 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
displaySettings.showDocumentSection ? 'lg:col-span-5' : 'lg:col-span-8'
}`}>
<Day1DocumentViewer document={selectedStandardDoc} />
@@ -325,8 +325,8 @@ export default function QualityInspectionPage() {
onSearchChange={handleSearchChange}
/>
<div className="flex-1 grid grid-cols-12 gap-6 lg:min-h-0">
<div className="col-span-12 lg:col-span-3 min-h-[300px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:gap-6 lg:min-h-0">
<div className="col-span-12 lg:col-span-3 min-h-[250px] sm:min-h-[300px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
<ReportList
reports={filteredReports}
selectedId={selectedReport?.id || null}
@@ -334,7 +334,7 @@ export default function QualityInspectionPage() {
/>
</div>
<div className="col-span-12 lg:col-span-4 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
<div className="col-span-12 lg:col-span-4 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
<RouteList
routes={currentRoutes}
selectedId={selectedRoute?.id || null}
@@ -344,7 +344,7 @@ export default function QualityInspectionPage() {
/>
</div>
<div className="col-span-12 lg:col-span-5 min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
<div className="col-span-12 lg:col-span-5 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
<DocumentList
documents={currentDocuments}
routeCode={selectedRoute?.code || null}