refactor: 전체 프론트엔드 하드코딩 색상 → CSS 변수 일괄 치환
447+ 파일, 4500+ 줄 변경: - gray-* → border/bg-muted/text-foreground/text-muted-foreground - blue-* → primary/ring - red-* → destructive - green-* → emerald (일관성) - indigo-* → primary - yellow/orange → amber (통일) - dark mode 변형도 시맨틱 토큰으로 변환 Made-with: Cursor
This commit is contained in:
parent
d967cf0a0d
commit
4f10b5e42d
|
|
@ -69,33 +69,33 @@ const RESOURCE_TYPE_CONFIG: Record<
|
||||||
string,
|
string,
|
||||||
{ label: string; icon: React.ElementType; color: string }
|
{ label: string; icon: React.ElementType; color: string }
|
||||||
> = {
|
> = {
|
||||||
MENU: { label: "메뉴", icon: Layout, color: "bg-blue-100 text-blue-700" },
|
MENU: { label: "메뉴", icon: Layout, color: "bg-primary/10 text-primary" },
|
||||||
SCREEN: { label: "화면", icon: Monitor, color: "bg-purple-100 text-purple-700" },
|
SCREEN: { label: "화면", icon: Monitor, color: "bg-purple-100 text-purple-700" },
|
||||||
SCREEN_LAYOUT: { label: "레이아웃", icon: Monitor, color: "bg-purple-100 text-purple-700" },
|
SCREEN_LAYOUT: { label: "레이아웃", icon: Monitor, color: "bg-purple-100 text-purple-700" },
|
||||||
FLOW: { label: "플로우", icon: GitBranch, color: "bg-green-100 text-green-700" },
|
FLOW: { label: "플로우", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" },
|
||||||
FLOW_STEP: { label: "플로우 스텝", icon: GitBranch, color: "bg-green-100 text-green-700" },
|
FLOW_STEP: { label: "플로우 스텝", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" },
|
||||||
USER: { label: "사용자", icon: User, color: "bg-orange-100 text-orange-700" },
|
USER: { label: "사용자", icon: User, color: "bg-amber-100 text-orange-700" },
|
||||||
ROLE: { label: "권한", icon: Shield, color: "bg-red-100 text-red-700" },
|
ROLE: { label: "권한", icon: Shield, color: "bg-destructive/10 text-destructive" },
|
||||||
PERMISSION: { label: "권한", icon: Shield, color: "bg-red-100 text-red-700" },
|
PERMISSION: { label: "권한", icon: Shield, color: "bg-destructive/10 text-destructive" },
|
||||||
COMPANY: { label: "회사", icon: Building2, color: "bg-indigo-100 text-indigo-700" },
|
COMPANY: { label: "회사", icon: Building2, color: "bg-indigo-100 text-indigo-700" },
|
||||||
CODE_CATEGORY: { label: "코드 카테고리", icon: Hash, color: "bg-cyan-100 text-cyan-700" },
|
CODE_CATEGORY: { label: "코드 카테고리", icon: Hash, color: "bg-cyan-100 text-cyan-700" },
|
||||||
CODE: { label: "코드", icon: Hash, color: "bg-cyan-100 text-cyan-700" },
|
CODE: { label: "코드", icon: Hash, color: "bg-cyan-100 text-cyan-700" },
|
||||||
DATA: { label: "데이터", icon: Database, color: "bg-gray-100 text-gray-700" },
|
DATA: { label: "데이터", icon: Database, color: "bg-muted text-foreground" },
|
||||||
TABLE: { label: "테이블", icon: Database, color: "bg-gray-100 text-gray-700" },
|
TABLE: { label: "테이블", icon: Database, color: "bg-muted text-foreground" },
|
||||||
NUMBERING_RULE: { label: "채번 규칙", icon: FileText, color: "bg-amber-100 text-amber-700" },
|
NUMBERING_RULE: { label: "채번 규칙", icon: FileText, color: "bg-amber-100 text-amber-700" },
|
||||||
BATCH: { label: "배치", icon: RefreshCw, color: "bg-teal-100 text-teal-700" },
|
BATCH: { label: "배치", icon: RefreshCw, color: "bg-teal-100 text-teal-700" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const ACTION_CONFIG: Record<string, { label: string; color: string }> = {
|
const ACTION_CONFIG: Record<string, { label: string; color: string }> = {
|
||||||
CREATE: { label: "생성", color: "bg-emerald-100 text-emerald-700" },
|
CREATE: { label: "생성", color: "bg-emerald-100 text-emerald-700" },
|
||||||
UPDATE: { label: "수정", color: "bg-blue-100 text-blue-700" },
|
UPDATE: { label: "수정", color: "bg-primary/10 text-primary" },
|
||||||
DELETE: { label: "삭제", color: "bg-red-100 text-red-700" },
|
DELETE: { label: "삭제", color: "bg-destructive/10 text-destructive" },
|
||||||
COPY: { label: "복사", color: "bg-violet-100 text-violet-700" },
|
COPY: { label: "복사", color: "bg-violet-100 text-violet-700" },
|
||||||
LOGIN: { label: "로그인", color: "bg-gray-100 text-gray-700" },
|
LOGIN: { label: "로그인", color: "bg-muted text-foreground" },
|
||||||
STATUS_CHANGE: { label: "상태변경", color: "bg-amber-100 text-amber-700" },
|
STATUS_CHANGE: { label: "상태변경", color: "bg-amber-100 text-amber-700" },
|
||||||
BATCH_CREATE: { label: "배치생성", color: "bg-emerald-100 text-emerald-700" },
|
BATCH_CREATE: { label: "배치생성", color: "bg-emerald-100 text-emerald-700" },
|
||||||
BATCH_UPDATE: { label: "배치수정", color: "bg-blue-100 text-blue-700" },
|
BATCH_UPDATE: { label: "배치수정", color: "bg-primary/10 text-primary" },
|
||||||
BATCH_DELETE: { label: "배치삭제", color: "bg-red-100 text-red-700" },
|
BATCH_DELETE: { label: "배치삭제", color: "bg-destructive/10 text-destructive" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatDateTime(dateStr: string): string {
|
function formatDateTime(dateStr: string): string {
|
||||||
|
|
@ -203,12 +203,12 @@ function renderChanges(changes: Record<string, unknown>) {
|
||||||
<tr className="bg-muted/50">
|
<tr className="bg-muted/50">
|
||||||
<th className="px-3 py-1.5 text-left font-medium">항목</th>
|
<th className="px-3 py-1.5 text-left font-medium">항목</th>
|
||||||
{hasBefore && (
|
{hasBefore && (
|
||||||
<th className="px-3 py-1.5 text-left font-medium text-red-600">
|
<th className="px-3 py-1.5 text-left font-medium text-destructive">
|
||||||
변경 전
|
변경 전
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{hasAfter && (
|
{hasAfter && (
|
||||||
<th className="px-3 py-1.5 text-left font-medium text-blue-600">
|
<th className="px-3 py-1.5 text-left font-medium text-primary">
|
||||||
변경 후
|
변경 후
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
|
|
@ -234,7 +234,7 @@ function renderChanges(changes: Record<string, unknown>) {
|
||||||
{hasBefore && (
|
{hasBefore && (
|
||||||
<td className="px-3 py-1.5">
|
<td className="px-3 py-1.5">
|
||||||
{row.beforeVal !== null ? (
|
{row.beforeVal !== null ? (
|
||||||
<span className="rounded bg-red-50 px-1.5 py-0.5 text-red-700">
|
<span className="rounded bg-destructive/10 px-1.5 py-0.5 text-destructive">
|
||||||
{row.beforeVal}
|
{row.beforeVal}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -245,7 +245,7 @@ function renderChanges(changes: Record<string, unknown>) {
|
||||||
{hasAfter && (
|
{hasAfter && (
|
||||||
<td className="px-3 py-1.5">
|
<td className="px-3 py-1.5">
|
||||||
{row.afterVal !== null ? (
|
{row.afterVal !== null ? (
|
||||||
<span className="rounded bg-blue-50 px-1.5 py-0.5 text-blue-700">
|
<span className="rounded bg-primary/10 px-1.5 py-0.5 text-primary">
|
||||||
{row.afterVal}
|
{row.afterVal}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -311,10 +311,10 @@ export default function BatchCreatePage() {
|
||||||
{/* 매핑 설정 */}
|
{/* 매핑 설정 */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* FROM 섹션 */}
|
{/* FROM 섹션 */}
|
||||||
<Card className="border-green-200">
|
<Card className="border-emerald-200">
|
||||||
<CardHeader className="bg-green-50">
|
<CardHeader className="bg-emerald-50">
|
||||||
<CardTitle className="text-green-700">FROM (원본 데이터베이스)</CardTitle>
|
<CardTitle className="text-emerald-700">FROM (원본 데이터베이스)</CardTitle>
|
||||||
<p className="text-sm text-green-600">
|
<p className="text-sm text-emerald-600">
|
||||||
1단계: 커넥션을 선택하세요 → 2단계: 테이블을 선택하세요 → 3단계: 컬럼을 클릭해서 매핑하세요
|
1단계: 커넥션을 선택하세요 → 2단계: 테이블을 선택하세요 → 3단계: 컬럼을 클릭해서 매핑하세요
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -365,7 +365,7 @@ export default function BatchCreatePage() {
|
||||||
{/* FROM 컬럼 목록 */}
|
{/* FROM 컬럼 목록 */}
|
||||||
{fromTable && (
|
{fromTable && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-blue-600 font-semibold">{fromTable} 테이블</Label>
|
<Label className="text-primary font-semibold">{fromTable} 테이블</Label>
|
||||||
<div className="border rounded-lg p-4 max-h-80 overflow-y-auto space-y-2">
|
<div className="border rounded-lg p-4 max-h-80 overflow-y-auto space-y-2">
|
||||||
{fromColumns.map((column) => (
|
{fromColumns.map((column) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -373,16 +373,16 @@ export default function BatchCreatePage() {
|
||||||
onClick={() => handleFromColumnClick(column)}
|
onClick={() => handleFromColumnClick(column)}
|
||||||
className={`p-3 border rounded cursor-pointer transition-colors ${
|
className={`p-3 border rounded cursor-pointer transition-colors ${
|
||||||
selectedFromColumn?.column_name === column.column_name
|
selectedFromColumn?.column_name === column.column_name
|
||||||
? 'bg-green-100 border-green-300'
|
? 'bg-emerald-100 border-green-300'
|
||||||
: 'hover:bg-gray-50 border-gray-200'
|
: 'hover:bg-muted border-border'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="font-medium">{column.column_name}</div>
|
<div className="font-medium">{column.column_name}</div>
|
||||||
<div className="text-sm text-gray-500">{column.data_type}</div>
|
<div className="text-sm text-muted-foreground">{column.data_type}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{fromColumns.length === 0 && fromTable && (
|
{fromColumns.length === 0 && fromTable && (
|
||||||
<div className="text-center text-gray-500 py-4">
|
<div className="text-center text-muted-foreground py-4">
|
||||||
컬럼을 불러오는 중...
|
컬럼을 불러오는 중...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -393,10 +393,10 @@ export default function BatchCreatePage() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* TO 섹션 */}
|
{/* TO 섹션 */}
|
||||||
<Card className="border-red-200">
|
<Card className="border-destructive/20">
|
||||||
<CardHeader className="bg-red-50">
|
<CardHeader className="bg-destructive/10">
|
||||||
<CardTitle className="text-red-700">TO (대상 데이터베이스)</CardTitle>
|
<CardTitle className="text-destructive">TO (대상 데이터베이스)</CardTitle>
|
||||||
<p className="text-sm text-red-600">
|
<p className="text-sm text-destructive">
|
||||||
FROM에서 컬럼을 선택한 후, 여기서 대상 컬럼을 클릭하면 매핑됩니다
|
FROM에서 컬럼을 선택한 후, 여기서 대상 컬럼을 클릭하면 매핑됩니다
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -447,7 +447,7 @@ export default function BatchCreatePage() {
|
||||||
{/* TO 컬럼 목록 */}
|
{/* TO 컬럼 목록 */}
|
||||||
{toTable && (
|
{toTable && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-blue-600 font-semibold">{toTable} 테이블</Label>
|
<Label className="text-primary font-semibold">{toTable} 테이블</Label>
|
||||||
<div className="border rounded-lg p-4 max-h-80 overflow-y-auto space-y-2">
|
<div className="border rounded-lg p-4 max-h-80 overflow-y-auto space-y-2">
|
||||||
{toColumns.map((column) => (
|
{toColumns.map((column) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -455,16 +455,16 @@ export default function BatchCreatePage() {
|
||||||
onClick={() => handleToColumnClick(column)}
|
onClick={() => handleToColumnClick(column)}
|
||||||
className={`p-3 border rounded cursor-pointer transition-colors ${
|
className={`p-3 border rounded cursor-pointer transition-colors ${
|
||||||
selectedFromColumn
|
selectedFromColumn
|
||||||
? 'hover:bg-red-50 border-gray-200'
|
? 'hover:bg-destructive/10 border-border'
|
||||||
: 'bg-gray-100 border-gray-300 cursor-not-allowed'
|
: 'bg-muted border-input cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="font-medium">{column.column_name}</div>
|
<div className="font-medium">{column.column_name}</div>
|
||||||
<div className="text-sm text-gray-500">{column.data_type}</div>
|
<div className="text-sm text-muted-foreground">{column.data_type}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{toColumns.length === 0 && toTable && (
|
{toColumns.length === 0 && toTable && (
|
||||||
<div className="text-center text-gray-500 py-4">
|
<div className="text-center text-muted-foreground py-4">
|
||||||
컬럼을 불러오는 중...
|
컬럼을 불러오는 중...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -484,22 +484,22 @@ export default function BatchCreatePage() {
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{mappings.map((mapping, index) => (
|
{mappings.map((mapping, index) => (
|
||||||
<div key={index} className="flex items-center justify-between p-4 border rounded-lg bg-yellow-50">
|
<div key={index} className="flex items-center justify-between p-4 border rounded-lg bg-amber-50">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{mapping.from_table_name}.{mapping.from_column_name}
|
{mapping.from_table_name}.{mapping.from_column_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-500">
|
<div className="text-muted-foreground">
|
||||||
{mapping.from_column_type}
|
{mapping.from_column_type}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRight className="h-4 w-4 text-gray-400" />
|
<ArrowRight className="h-4 w-4 text-muted-foreground/70" />
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{mapping.to_table_name}.{mapping.to_column_name}
|
{mapping.to_table_name}.{mapping.to_column_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-500">
|
<div className="text-muted-foreground">
|
||||||
{mapping.to_column_type}
|
{mapping.to_column_type}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -508,7 +508,7 @@ export default function BatchCreatePage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeMapping(index)}
|
onClick={() => removeMapping(index)}
|
||||||
className="text-red-600 hover:text-red-700"
|
className="text-destructive hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -815,7 +815,7 @@ export default function BatchEditPage() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{authTokenMode === "direct"
|
{authTokenMode === "direct"
|
||||||
? "API 호출 시 Authorization 헤더에 사용할 토큰을 입력하세요."
|
? "API 호출 시 Authorization 헤더에 사용할 토큰을 입력하세요."
|
||||||
: "auth_tokens 테이블에서 선택한 서비스의 최신 토큰을 사용합니다."}
|
: "auth_tokens 테이블에서 선택한 서비스의 최신 토큰을 사용합니다."}
|
||||||
|
|
@ -874,7 +874,7 @@ export default function BatchEditPage() {
|
||||||
onChange={(e) => setDataArrayPath(e.target.value)}
|
onChange={(e) => setDataArrayPath(e.target.value)}
|
||||||
placeholder="response (예: data.items, results)"
|
placeholder="response (예: data.items, results)"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
API 응답에서 배열 데이터가 있는 경로를 입력하세요. 비워두면 응답 전체를 사용합니다.
|
API 응답에서 배열 데이터가 있는 경로를 입력하세요. 비워두면 응답 전체를 사용합니다.
|
||||||
<br />
|
<br />
|
||||||
예시: response, data.items, result.list
|
예시: response, data.items, result.list
|
||||||
|
|
@ -902,7 +902,7 @@ export default function BatchEditPage() {
|
||||||
className="min-h-[100px]"
|
className="min-h-[100px]"
|
||||||
rows={5}
|
rows={5}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">API 호출 시 함께 전송할 JSON 데이터를 입력하세요.</p>
|
<p className="mt-1 text-xs text-muted-foreground">API 호출 시 함께 전송할 JSON 데이터를 입력하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -910,7 +910,7 @@ export default function BatchEditPage() {
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<Label className="text-base font-medium">API 파라미터 설정</Label>
|
<Label className="text-base font-medium">API 파라미터 설정</Label>
|
||||||
<p className="mt-1 text-sm text-gray-600">특정 사용자나 조건으로 데이터를 조회할 때 사용합니다.</p>
|
<p className="mt-1 text-sm text-muted-foreground">특정 사용자나 조건으로 데이터를 조회할 때 사용합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -967,26 +967,26 @@ export default function BatchEditPage() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{apiParamSource === "dynamic" && (
|
{apiParamSource === "dynamic" && (
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
동적값은 배치 실행 시 설정된 값으로 치환됩니다. 예: {"{{user_id}}"} → 실제 사용자 ID
|
동적값은 배치 실행 시 설정된 값으로 치환됩니다. 예: {"{{user_id}}"} → 실제 사용자 ID
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{apiParamType === "url" && (
|
{apiParamType === "url" && (
|
||||||
<div className="rounded-lg bg-blue-50 p-3">
|
<div className="rounded-lg bg-primary/10 p-3">
|
||||||
<div className="text-sm font-medium text-blue-800">URL 파라미터 예시</div>
|
<div className="text-sm font-medium text-primary">URL 파라미터 예시</div>
|
||||||
<div className="mt-1 text-sm text-blue-700">
|
<div className="mt-1 text-sm text-primary">
|
||||||
엔드포인트: /api/users/{`{${apiParamName || "userId"}}`}
|
엔드포인트: /api/users/{`{${apiParamName || "userId"}}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-blue-700">실제 호출: /api/users/{apiParamValue || "123"}</div>
|
<div className="text-sm text-primary">실제 호출: /api/users/{apiParamValue || "123"}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{apiParamType === "query" && (
|
{apiParamType === "query" && (
|
||||||
<div className="rounded-lg bg-green-50 p-3">
|
<div className="rounded-lg bg-emerald-50 p-3">
|
||||||
<div className="text-sm font-medium text-green-800">쿼리 파라미터 예시</div>
|
<div className="text-sm font-medium text-emerald-800">쿼리 파라미터 예시</div>
|
||||||
<div className="mt-1 text-sm text-green-700">
|
<div className="mt-1 text-sm text-emerald-700">
|
||||||
실제 호출: {mappings[0]?.from_table_name || "/api/users"}?{apiParamName || "userId"}=
|
실제 호출: {mappings[0]?.from_table_name || "/api/users"}?{apiParamName || "userId"}=
|
||||||
{apiParamValue || "123"}
|
{apiParamValue || "123"}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,7 @@ export default function FlowEditorPage() {
|
||||||
onNodeDragStop={handleNodeDragStop}
|
onNodeDragStop={handleNodeDragStop}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
fitView
|
fitView
|
||||||
className="bg-gray-50"
|
className="bg-muted"
|
||||||
>
|
>
|
||||||
<Background />
|
<Background />
|
||||||
<Controls />
|
<Controls />
|
||||||
|
|
|
||||||
|
|
@ -996,7 +996,7 @@ export default function FlowManagementPage() {
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium">{table.displayName || table.tableName}</span>
|
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||||||
{table.description && (
|
{table.description && (
|
||||||
<span className="text-[10px] text-gray-500">{table.description}</span>
|
<span className="text-[10px] text-muted-foreground">{table.description}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
|
|
||||||
|
|
@ -409,7 +409,7 @@ example2@example.com,김철수,XYZ회사`;
|
||||||
{recipients.length > 0 && (
|
{recipients.length > 0 && (
|
||||||
<div className="rounded-md border bg-muted p-4">
|
<div className="rounded-md border bg-muted p-4">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
|
||||||
<span className="font-medium">{recipients.length}명의 수신자</span>
|
<span className="font-medium">{recipients.length}명의 수신자</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,8 @@ export default function MailDashboardPage() {
|
||||||
value: stats.totalAccounts,
|
value: stats.totalAccounts,
|
||||||
icon: Users,
|
icon: Users,
|
||||||
color: "blue",
|
color: "blue",
|
||||||
bgColor: "bg-blue-100",
|
bgColor: "bg-primary/10",
|
||||||
iconColor: "text-blue-600",
|
iconColor: "text-primary",
|
||||||
href: "/admin/mail/accounts",
|
href: "/admin/mail/accounts",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -110,8 +110,8 @@ export default function MailDashboardPage() {
|
||||||
value: stats.totalTemplates,
|
value: stats.totalTemplates,
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
color: "green",
|
color: "green",
|
||||||
bgColor: "bg-green-100",
|
bgColor: "bg-emerald-100",
|
||||||
iconColor: "text-green-600",
|
iconColor: "text-emerald-600",
|
||||||
href: "/admin/mail/templates",
|
href: "/admin/mail/templates",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -119,8 +119,8 @@ export default function MailDashboardPage() {
|
||||||
value: stats.sentToday,
|
value: stats.sentToday,
|
||||||
icon: Send,
|
icon: Send,
|
||||||
color: "orange",
|
color: "orange",
|
||||||
bgColor: "bg-orange-100",
|
bgColor: "bg-amber-100",
|
||||||
iconColor: "text-orange-600",
|
iconColor: "text-amber-600",
|
||||||
href: "/admin/mail/sent",
|
href: "/admin/mail/sent",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -438,8 +438,8 @@ export default function MailReceivePage() {
|
||||||
<div
|
<div
|
||||||
className={`mt-4 p-3 rounded-lg flex items-center gap-2 ${
|
className={`mt-4 p-3 rounded-lg flex items-center gap-2 ${
|
||||||
testResult.success
|
testResult.success
|
||||||
? "bg-green-50 text-green-800 border border-green-200"
|
? "bg-emerald-50 text-emerald-800 border border-emerald-200"
|
||||||
: "bg-red-50 text-red-800 border border-red-200"
|
: "bg-destructive/10 text-red-800 border border-destructive/20"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{testResult.success ? (
|
{testResult.success ? (
|
||||||
|
|
@ -460,7 +460,7 @@ export default function MailReceivePage() {
|
||||||
<div className="flex flex-col md:flex-row gap-3">
|
<div className="flex flex-col md:flex-row gap-3">
|
||||||
{/* 검색 */}
|
{/* 검색 */}
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/70" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
|
|
@ -511,7 +511,7 @@ export default function MailReceivePage() {
|
||||||
{filteredAndSortedMails.length}개의 메일이 검색되었습니다
|
{filteredAndSortedMails.length}개의 메일이 검색되었습니다
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<span className="ml-2">
|
<span className="ml-2">
|
||||||
(검색어: <span className="font-medium text-orange-600">{searchTerm}</span>)
|
(검색어: <span className="font-medium text-amber-600">{searchTerm}</span>)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -527,14 +527,14 @@ export default function MailReceivePage() {
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Card className="">
|
<Card className="">
|
||||||
<CardContent className="flex justify-center items-center py-16">
|
<CardContent className="flex justify-center items-center py-16">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-orange-500" />
|
<Loader2 className="w-8 h-8 animate-spin text-amber-500" />
|
||||||
<span className="ml-3 text-muted-foreground">메일을 불러오는 중...</span>
|
<span className="ml-3 text-muted-foreground">메일을 불러오는 중...</span>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : filteredAndSortedMails.length === 0 ? (
|
) : filteredAndSortedMails.length === 0 ? (
|
||||||
<Card className="text-center py-16 bg-card ">
|
<Card className="text-center py-16 bg-card ">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<Mail className="w-16 h-16 mx-auto mb-4 text-gray-300" />
|
<Mail className="w-16 h-16 mx-auto mb-4 text-muted-foreground/50" />
|
||||||
<p className="text-muted-foreground mb-4">
|
<p className="text-muted-foreground mb-4">
|
||||||
{!selectedAccountId
|
{!selectedAccountId
|
||||||
? "메일 계정을 선택하세요"
|
? "메일 계정을 선택하세요"
|
||||||
|
|
@ -560,9 +560,9 @@ export default function MailReceivePage() {
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className="">
|
<Card className="">
|
||||||
<CardHeader className="bg-gradient-to-r from-slate-50 to-gray-50 border-b">
|
<CardHeader className="bg-gradient-to-r from-slate-50 to-muted border-b">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Inbox className="w-5 h-5 text-orange-500" />
|
<Inbox className="w-5 h-5 text-amber-500" />
|
||||||
받은 메일함 ({filteredAndSortedMails.length}/{mails.length}개)
|
받은 메일함 ({filteredAndSortedMails.length}/{mails.length}개)
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -573,14 +573,14 @@ export default function MailReceivePage() {
|
||||||
key={mail.id}
|
key={mail.id}
|
||||||
onClick={() => handleMailClick(mail)}
|
onClick={() => handleMailClick(mail)}
|
||||||
className={`p-4 hover:bg-background transition-colors cursor-pointer ${
|
className={`p-4 hover:bg-background transition-colors cursor-pointer ${
|
||||||
!mail.isRead ? "bg-blue-50/30" : ""
|
!mail.isRead ? "bg-primary/10/30" : ""
|
||||||
} ${selectedMailId === mail.id ? "bg-accent border-l-4 border-l-primary" : ""}`}
|
} ${selectedMailId === mail.id ? "bg-accent border-l-4 border-l-primary" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
{/* 읽음 표시 */}
|
{/* 읽음 표시 */}
|
||||||
<div className="flex-shrink-0 w-2 h-2 mt-2">
|
<div className="flex-shrink-0 w-2 h-2 mt-2">
|
||||||
{!mail.isRead && (
|
{!mail.isRead && (
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-primary rounded-full"></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -598,7 +598,7 @@ export default function MailReceivePage() {
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{mail.hasAttachments && (
|
{mail.hasAttachments && (
|
||||||
<Paperclip className="w-4 h-4 text-gray-400" />
|
<Paperclip className="w-4 h-4 text-muted-foreground/70" />
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{formatDate(mail.date)}
|
{formatDate(mail.date)}
|
||||||
|
|
@ -882,14 +882,14 @@ export default function MailReceivePage() {
|
||||||
) : loadingDetail ? (
|
) : loadingDetail ? (
|
||||||
<Card className="sticky top-6">
|
<Card className="sticky top-6">
|
||||||
<CardContent className="flex justify-center items-center py-16">
|
<CardContent className="flex justify-center items-center py-16">
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-orange-500" />
|
<Loader2 className="w-8 h-8 animate-spin text-amber-500" />
|
||||||
<span className="ml-3 text-muted-foreground">메일을 불러오는 중...</span>
|
<span className="ml-3 text-muted-foreground">메일을 불러오는 중...</span>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className="sticky top-6">
|
<Card className="sticky top-6">
|
||||||
<CardContent className="flex flex-col justify-center items-center py-16 text-center">
|
<CardContent className="flex flex-col justify-center items-center py-16 text-center">
|
||||||
<Mail className="w-16 h-16 mb-4 text-gray-300" />
|
<Mail className="w-16 h-16 mb-4 text-muted-foreground/50" />
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
메일을 선택하면 내용이 표시됩니다
|
메일을 선택하면 내용이 표시됩니다
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -900,10 +900,10 @@ export default function MailReceivePage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 안내 정보 */}
|
{/* 안내 정보 */}
|
||||||
<Card className="bg-gradient-to-r from-green-50 to-emerald-50 border-green-200 ">
|
<Card className="bg-gradient-to-r from-green-50 to-emerald-50 border-emerald-200 ">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg flex items-center">
|
<CardTitle className="text-lg flex items-center">
|
||||||
<CheckCircle className="w-5 h-5 mr-2 text-green-600" />
|
<CheckCircle className="w-5 h-5 mr-2 text-emerald-600" />
|
||||||
메일 수신 기능 완성! 🎉
|
메일 수신 기능 완성! 🎉
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -913,81 +913,81 @@ export default function MailReceivePage() {
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-800 mb-2">📬 기본 기능</p>
|
<p className="font-medium text-foreground mb-2">📬 기본 기능</p>
|
||||||
<ul className="space-y-1 text-sm text-muted-foreground">
|
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>IMAP 프로토콜 메일 수신</span>
|
<span>IMAP 프로토콜 메일 수신</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>메일 목록 표시</span>
|
<span>메일 목록 표시</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>읽음/안읽음 상태</span>
|
<span>읽음/안읽음 상태</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>첨부파일 유무 표시</span>
|
<span>첨부파일 유무 표시</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-800 mb-2">📄 상세보기</p>
|
<p className="font-medium text-foreground mb-2">📄 상세보기</p>
|
||||||
<ul className="space-y-1 text-sm text-muted-foreground">
|
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>HTML 본문 렌더링</span>
|
<span>HTML 본문 렌더링</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>텍스트 본문 보기</span>
|
<span>텍스트 본문 보기</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>자동 읽음 처리</span>
|
<span>자동 읽음 처리</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>첨부파일 다운로드</span>
|
<span>첨부파일 다운로드</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-800 mb-2">🔍 고급 기능</p>
|
<p className="font-medium text-foreground mb-2">🔍 고급 기능</p>
|
||||||
<ul className="space-y-1 text-sm text-muted-foreground">
|
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>통합 검색 (제목/발신자/내용)</span>
|
<span>통합 검색 (제목/발신자/내용)</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>필터링 (읽음/첨부파일)</span>
|
<span>필터링 (읽음/첨부파일)</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>정렬 (날짜/발신자)</span>
|
<span>정렬 (날짜/발신자)</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>자동 새로고침 (30초)</span>
|
<span>자동 새로고침 (30초)</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-800 mb-2">🔒 보안</p>
|
<p className="font-medium text-foreground mb-2">🔒 보안</p>
|
||||||
<ul className="space-y-1 text-sm text-muted-foreground">
|
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>XSS 방지 (DOMPurify)</span>
|
<span>XSS 방지 (DOMPurify)</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>비밀번호 암호화</span>
|
<span>비밀번호 암호화</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="text-green-500 mr-2">✓</span>
|
<span className="text-emerald-500 mr-2">✓</span>
|
||||||
<span>안전한 파일명 생성</span>
|
<span>안전한 파일명 생성</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -516,12 +516,12 @@ ${data.originalBody}`;
|
||||||
toast({
|
toast({
|
||||||
title: (
|
title: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="w-5 h-5 text-green-500" />
|
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||||
<span>메일 발송 완료!</span>
|
<span>메일 발송 완료!</span>
|
||||||
</div>
|
</div>
|
||||||
) as any,
|
) as any,
|
||||||
description: `${to.length}명${cc.length > 0 ? ` (참조 ${cc.length}명)` : ""}${bcc.length > 0 ? ` (숨은참조 ${bcc.length}명)` : ""}${attachments.length > 0 ? ` (첨부파일 ${attachments.length}개)` : ""}에게 메일이 성공적으로 발송되었습니다.`,
|
description: `${to.length}명${cc.length > 0 ? ` (참조 ${cc.length}명)` : ""}${bcc.length > 0 ? ` (숨은참조 ${bcc.length}명)` : ""}${attachments.length > 0 ? ` (첨부파일 ${attachments.length}개)` : ""}에게 메일이 성공적으로 발송되었습니다.`,
|
||||||
className: "border-green-500 bg-green-50",
|
className: "border-emerald-500 bg-emerald-50",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 알림 갱신 이벤트 발생
|
// 알림 갱신 이벤트 발생
|
||||||
|
|
@ -544,7 +544,7 @@ ${data.originalBody}`;
|
||||||
toast({
|
toast({
|
||||||
title: (
|
title: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertCircle className="w-5 h-5 text-red-500" />
|
<AlertCircle className="w-5 h-5 text-destructive" />
|
||||||
<span>메일 발송 실패</span>
|
<span>메일 발송 실패</span>
|
||||||
</div>
|
</div>
|
||||||
) as any,
|
) as any,
|
||||||
|
|
@ -781,7 +781,7 @@ ${data.originalBody}`;
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
||||||
<span>
|
<span>
|
||||||
{new Date(lastSaved).toLocaleTimeString('ko-KR', {
|
{new Date(lastSaved).toLocaleTimeString('ko-KR', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
|
|
@ -895,7 +895,7 @@ ${data.originalBody}`;
|
||||||
{to.map((email) => (
|
{to.map((email) => (
|
||||||
<div
|
<div
|
||||||
key={email}
|
key={email}
|
||||||
className="flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-700 rounded-md text-sm"
|
className="flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm"
|
||||||
>
|
>
|
||||||
<span>{email}</span>
|
<span>{email}</span>
|
||||||
<button
|
<button
|
||||||
|
|
@ -933,12 +933,12 @@ ${data.originalBody}`;
|
||||||
{cc.map((email) => (
|
{cc.map((email) => (
|
||||||
<div
|
<div
|
||||||
key={email}
|
key={email}
|
||||||
className="flex items-center gap-1 px-2 py-1 bg-green-100 text-green-700 rounded-md text-sm"
|
className="flex items-center gap-1 px-2 py-1 bg-emerald-100 text-emerald-700 rounded-md text-sm"
|
||||||
>
|
>
|
||||||
<span>{email}</span>
|
<span>{email}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeEmail(email, "cc")}
|
onClick={() => removeEmail(email, "cc")}
|
||||||
className="hover:bg-green-200 rounded p-0.5"
|
className="hover:bg-emerald-200 rounded p-0.5"
|
||||||
>
|
>
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1222,7 +1222,7 @@ ${data.originalBody}`;
|
||||||
<div
|
<div
|
||||||
key={component.id}
|
key={component.id}
|
||||||
style={{ height: `${component.height || 20}px` }}
|
style={{ height: `${component.height || 20}px` }}
|
||||||
className="bg-background rounded flex items-center justify-center text-xs text-gray-400"
|
className="bg-background rounded flex items-center justify-center text-xs text-muted-foreground/70"
|
||||||
>
|
>
|
||||||
여백
|
여백
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1236,7 +1236,7 @@ ${data.originalBody}`;
|
||||||
{component.logoSrc && <img src={component.logoSrc} alt="로고" className="h-10" />}
|
{component.logoSrc && <img src={component.logoSrc} alt="로고" className="h-10" />}
|
||||||
<span className="font-bold text-lg">{component.brandName}</span>
|
<span className="font-bold text-lg">{component.brandName}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-gray-500">{component.sendDate}</span>
|
<span className="text-sm text-muted-foreground">{component.sendDate}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -1245,13 +1245,13 @@ ${data.originalBody}`;
|
||||||
return (
|
return (
|
||||||
<div key={component.id} className="border rounded-lg overflow-hidden">
|
<div key={component.id} className="border rounded-lg overflow-hidden">
|
||||||
{component.tableTitle && (
|
{component.tableTitle && (
|
||||||
<div className="bg-gray-50 px-4 py-2 font-semibold border-b">{component.tableTitle}</div>
|
<div className="bg-muted px-4 py-2 font-semibold border-b">{component.tableTitle}</div>
|
||||||
)}
|
)}
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<tbody>
|
<tbody>
|
||||||
{component.rows?.map((row: any, i: number) => (
|
{component.rows?.map((row: any, i: number) => (
|
||||||
<tr key={i} className={i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
<tr key={i} className={i % 2 === 0 ? 'bg-white' : 'bg-muted'}>
|
||||||
<td className="px-4 py-2 font-medium text-gray-600 w-1/3 border-r">{row.label}</td>
|
<td className="px-4 py-2 font-medium text-muted-foreground w-1/3 border-r">{row.label}</td>
|
||||||
<td className="px-4 py-2">{row.value}</td>
|
<td className="px-4 py-2">{row.value}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1263,9 +1263,9 @@ ${data.originalBody}`;
|
||||||
case 'alertBox':
|
case 'alertBox':
|
||||||
return (
|
return (
|
||||||
<div key={component.id} className={`p-4 rounded-lg border-l-4 ${
|
<div key={component.id} className={`p-4 rounded-lg border-l-4 ${
|
||||||
component.alertType === 'info' ? 'bg-blue-50 border-blue-500 text-blue-800' :
|
component.alertType === 'info' ? 'bg-primary/10 border-primary text-primary' :
|
||||||
component.alertType === 'warning' ? 'bg-amber-50 border-amber-500 text-amber-800' :
|
component.alertType === 'warning' ? 'bg-amber-50 border-amber-500 text-amber-800' :
|
||||||
component.alertType === 'danger' ? 'bg-red-50 border-red-500 text-red-800' :
|
component.alertType === 'danger' ? 'bg-destructive/10 border-destructive text-red-800' :
|
||||||
'bg-emerald-50 border-emerald-500 text-emerald-800'
|
'bg-emerald-50 border-emerald-500 text-emerald-800'
|
||||||
}`}>
|
}`}>
|
||||||
{component.alertTitle && <div className="font-bold mb-1">{component.alertTitle}</div>}
|
{component.alertTitle && <div className="font-bold mb-1">{component.alertTitle}</div>}
|
||||||
|
|
@ -1275,13 +1275,13 @@ ${data.originalBody}`;
|
||||||
|
|
||||||
case 'divider':
|
case 'divider':
|
||||||
return (
|
return (
|
||||||
<hr key={component.id} className="border-gray-300" style={{ borderWidth: `${component.height || 1}px` }} />
|
<hr key={component.id} className="border-input" style={{ borderWidth: `${component.height || 1}px` }} />
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'footer':
|
case 'footer':
|
||||||
return (
|
return (
|
||||||
<div key={component.id} className="text-center text-sm text-gray-500 py-4 border-t bg-gray-50">
|
<div key={component.id} className="text-center text-sm text-muted-foreground py-4 border-t bg-muted">
|
||||||
{component.companyName && <div className="font-semibold text-gray-700">{component.companyName}</div>}
|
{component.companyName && <div className="font-semibold text-foreground">{component.companyName}</div>}
|
||||||
{(component.ceoName || component.businessNumber) && (
|
{(component.ceoName || component.businessNumber) && (
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
{component.ceoName && <span>대표: {component.ceoName}</span>}
|
{component.ceoName && <span>대표: {component.ceoName}</span>}
|
||||||
|
|
@ -1297,7 +1297,7 @@ ${data.originalBody}`;
|
||||||
{component.email && <span>Email: {component.email}</span>}
|
{component.email && <span>Email: {component.email}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{component.copyright && <div className="mt-2 text-xs text-gray-400">{component.copyright}</div>}
|
{component.copyright && <div className="mt-2 text-xs text-muted-foreground/70">{component.copyright}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1318,9 +1318,9 @@ ${data.originalBody}`;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gradient-to-r from-green-50 to-emerald-50 px-4 py-3 border-t border-green-200">
|
<div className="bg-gradient-to-r from-green-50 to-emerald-50 px-4 py-3 border-t border-emerald-200">
|
||||||
<p className="text-sm text-green-800 flex items-center gap-2 font-medium">
|
<p className="text-sm text-emerald-800 flex items-center gap-2 font-medium">
|
||||||
<CheckCircle2 className="w-4 h-4 text-green-600" />
|
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
||||||
위 내용으로 메일이 발송됩니다
|
위 내용으로 메일이 발송됩니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1396,7 +1396,7 @@ ${data.originalBody}`;
|
||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
<Upload className="w-12 h-12 mx-auto text-gray-400 mb-3" />
|
<Upload className="w-12 h-12 mx-auto text-muted-foreground/70 mb-3" />
|
||||||
<p className="text-sm text-muted-foreground mb-1">
|
<p className="text-sm text-muted-foreground mb-1">
|
||||||
파일을 드래그하거나 클릭하여 선택하세요
|
파일을 드래그하거나 클릭하여 선택하세요
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -1430,7 +1430,7 @@ ${data.originalBody}`;
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeFile(index)}
|
onClick={() => removeFile(index)}
|
||||||
className="flex-shrink-0 text-red-500 hover:text-red-600 hover:bg-red-50"
|
className="flex-shrink-0 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -1530,7 +1530,7 @@ ${data.originalBody}`;
|
||||||
<div key={index} className="flex items-center gap-2 text-xs text-muted-foreground">
|
<div key={index} className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<File className="w-3 h-3" />
|
<File className="w-3 h-3" />
|
||||||
<span className="truncate">{file.name}</span>
|
<span className="truncate">{file.name}</span>
|
||||||
<span className="text-gray-400">({formatFileSize(file.size)})</span>
|
<span className="text-muted-foreground/70">({formatFileSize(file.size)})</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -371,8 +371,8 @@ export default function SentMailPage() {
|
||||||
{stats.successCount}
|
{stats.successCount}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-green-500/10 rounded-lg">
|
<div className="p-3 bg-emerald-500/10 rounded-lg">
|
||||||
<CheckCircle2 className="w-6 h-6 text-green-600" />
|
<CheckCircle2 className="w-6 h-6 text-emerald-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
@ -387,8 +387,8 @@ export default function SentMailPage() {
|
||||||
{stats.failedCount}
|
{stats.failedCount}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-red-500/10 rounded-lg">
|
<div className="p-3 bg-destructive/10 rounded-lg">
|
||||||
<XCircle className="w-6 h-6 text-red-600" />
|
<XCircle className="w-6 h-6 text-destructive" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
@ -403,8 +403,8 @@ export default function SentMailPage() {
|
||||||
{stats.todayCount}
|
{stats.todayCount}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-blue-500/10 rounded-lg">
|
<div className="p-3 bg-primary/10 rounded-lg">
|
||||||
<Calendar className="w-6 h-6 text-blue-600" />
|
<Calendar className="w-6 h-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -592,19 +592,19 @@ export default function BatchManagementNewPage() {
|
||||||
<div
|
<div
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className={`cursor-pointer rounded-lg border p-3 transition-all ${
|
className={`cursor-pointer rounded-lg border p-3 transition-all ${
|
||||||
batchType === option.value ? "border-blue-500 bg-blue-50" : "border-gray-200 hover:border-gray-300"
|
batchType === option.value ? "border-primary bg-primary/10" : "border-border hover:border-input"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setBatchType(option.value)}
|
onClick={() => setBatchType(option.value)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{option.value === "restapi-to-db" ? (
|
{option.value === "restapi-to-db" ? (
|
||||||
<Globe className="h-4 w-4 text-blue-600" />
|
<Globe className="h-4 w-4 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<Database className="h-4 w-4 text-green-600" />
|
<Database className="h-4 w-4 text-emerald-600" />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">{option.label}</div>
|
<div className="text-sm font-medium">{option.label}</div>
|
||||||
<div className="mt-1 text-xs text-gray-500">{option.description}</div>
|
<div className="mt-1 text-xs text-muted-foreground">{option.description}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -739,7 +739,7 @@ export default function BatchManagementNewPage() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{authTokenMode === "direct"
|
{authTokenMode === "direct"
|
||||||
? "API 호출 시 Authorization 헤더에 사용할 토큰을 입력하세요."
|
? "API 호출 시 Authorization 헤더에 사용할 토큰을 입력하세요."
|
||||||
: "auth_tokens 테이블에서 선택한 서비스의 최신 토큰을 사용합니다."}
|
: "auth_tokens 테이블에서 선택한 서비스의 최신 토큰을 사용합니다."}
|
||||||
|
|
@ -782,7 +782,7 @@ export default function BatchManagementNewPage() {
|
||||||
onChange={(e) => setDataArrayPath(e.target.value)}
|
onChange={(e) => setDataArrayPath(e.target.value)}
|
||||||
placeholder="response (예: data.items, results)"
|
placeholder="response (예: data.items, results)"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
API 응답에서 배열 데이터가 있는 경로를 입력하세요. 비워두면 응답 전체를 사용합니다.
|
API 응답에서 배열 데이터가 있는 경로를 입력하세요. 비워두면 응답 전체를 사용합니다.
|
||||||
<br />
|
<br />
|
||||||
예시: response, data.items, result.list
|
예시: response, data.items, result.list
|
||||||
|
|
@ -801,7 +801,7 @@ export default function BatchManagementNewPage() {
|
||||||
className="min-h-[100px]"
|
className="min-h-[100px]"
|
||||||
rows={5}
|
rows={5}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">API 호출 시 함께 전송할 JSON 데이터를 입력하세요.</p>
|
<p className="mt-1 text-xs text-muted-foreground">API 호출 시 함께 전송할 JSON 데이터를 입력하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -809,7 +809,7 @@ export default function BatchManagementNewPage() {
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<Label className="text-base font-medium">API 파라미터 설정</Label>
|
<Label className="text-base font-medium">API 파라미터 설정</Label>
|
||||||
<p className="mt-1 text-sm text-gray-600">특정 사용자나 조건으로 데이터를 조회할 때 사용합니다.</p>
|
<p className="mt-1 text-sm text-muted-foreground">특정 사용자나 조건으로 데이터를 조회할 때 사용합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -868,26 +868,26 @@ export default function BatchManagementNewPage() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{apiParamSource === "dynamic" && (
|
{apiParamSource === "dynamic" && (
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
동적값은 배치 실행 시 설정된 값으로 치환됩니다. 예: {"{{user_id}}"} → 실제 사용자 ID
|
동적값은 배치 실행 시 설정된 값으로 치환됩니다. 예: {"{{user_id}}"} → 실제 사용자 ID
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{apiParamType === "url" && (
|
{apiParamType === "url" && (
|
||||||
<div className="rounded-lg bg-blue-50 p-3">
|
<div className="rounded-lg bg-primary/10 p-3">
|
||||||
<div className="text-sm font-medium text-blue-800">URL 파라미터 예시</div>
|
<div className="text-sm font-medium text-primary">URL 파라미터 예시</div>
|
||||||
<div className="mt-1 text-sm text-blue-700">
|
<div className="mt-1 text-sm text-primary">
|
||||||
엔드포인트: /api/users/{`{${apiParamName || "userId"}}`}
|
엔드포인트: /api/users/{`{${apiParamName || "userId"}}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-blue-700">실제 호출: /api/users/{apiParamValue || "123"}</div>
|
<div className="text-sm text-primary">실제 호출: /api/users/{apiParamValue || "123"}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{apiParamType === "query" && (
|
{apiParamType === "query" && (
|
||||||
<div className="rounded-lg bg-green-50 p-3">
|
<div className="rounded-lg bg-emerald-50 p-3">
|
||||||
<div className="text-sm font-medium text-green-800">쿼리 파라미터 예시</div>
|
<div className="text-sm font-medium text-emerald-800">쿼리 파라미터 예시</div>
|
||||||
<div className="mt-1 text-sm text-green-700">
|
<div className="mt-1 text-sm text-emerald-700">
|
||||||
실제 호출: {fromEndpoint || "/api/users"}?{apiParamName || "userId"}=
|
실제 호출: {fromEndpoint || "/api/users"}?{apiParamName || "userId"}=
|
||||||
{apiParamValue || "123"}
|
{apiParamValue || "123"}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -899,9 +899,9 @@ export default function BatchManagementNewPage() {
|
||||||
|
|
||||||
{/* API 호출 미리보기 정보 */}
|
{/* API 호출 미리보기 정보 */}
|
||||||
{fromApiUrl && fromEndpoint && (
|
{fromApiUrl && fromEndpoint && (
|
||||||
<div className="rounded-lg bg-gray-50 p-3">
|
<div className="rounded-lg bg-muted p-3">
|
||||||
<div className="text-sm font-medium text-gray-700">API 호출 정보</div>
|
<div className="text-sm font-medium text-foreground">API 호출 정보</div>
|
||||||
<div className="mt-1 text-sm text-gray-600">
|
<div className="mt-1 text-sm text-muted-foreground">
|
||||||
{fromApiMethod} {fromApiUrl}
|
{fromApiMethod} {fromApiUrl}
|
||||||
{apiParamType === "url" && apiParamName && apiParamValue
|
{apiParamType === "url" && apiParamName && apiParamValue
|
||||||
? fromEndpoint.replace(`{${apiParamName}}`, apiParamValue) || fromEndpoint + `/${apiParamValue}`
|
? fromEndpoint.replace(`{${apiParamName}}`, apiParamValue) || fromEndpoint + `/${apiParamValue}`
|
||||||
|
|
@ -911,14 +911,14 @@ export default function BatchManagementNewPage() {
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
{((authTokenMode === "direct" && fromApiKey) || (authTokenMode === "db" && authServiceName)) && (
|
{((authTokenMode === "direct" && fromApiKey) || (authTokenMode === "db" && authServiceName)) && (
|
||||||
<div className="mt-1 text-xs text-gray-500">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
{authTokenMode === "direct"
|
{authTokenMode === "direct"
|
||||||
? `Authorization: Bearer ${fromApiKey.substring(0, 15)}...`
|
? `Authorization: Bearer ${fromApiKey.substring(0, 15)}...`
|
||||||
: `Authorization: DB 토큰 (${authServiceName})`}
|
: `Authorization: DB 토큰 (${authServiceName})`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{apiParamType !== "none" && apiParamName && apiParamValue && (
|
{apiParamType !== "none" && apiParamName && apiParamValue && (
|
||||||
<div className="mt-1 text-xs text-blue-600">
|
<div className="mt-1 text-xs text-primary">
|
||||||
파라미터: {apiParamName} = {apiParamValue} ({apiParamSource === "static" ? "고정값" : "동적값"})
|
파라미터: {apiParamName} = {apiParamValue} ({apiParamSource === "static" ? "고정값" : "동적값"})
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -988,7 +988,7 @@ export default function BatchManagementNewPage() {
|
||||||
setSelectedColumns(selectedColumns.filter((col) => col !== column.column_name));
|
setSelectedColumns(selectedColumns.filter((col) => col !== column.column_name));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="rounded border-gray-300"
|
className="rounded border-input"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor={`col-${column.column_name}`}
|
htmlFor={`col-${column.column_name}`}
|
||||||
|
|
@ -1002,14 +1002,14 @@ export default function BatchManagementNewPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 선택된 컬럼 개수 표시 */}
|
{/* 선택된 컬럼 개수 표시 */}
|
||||||
<div className="mt-2 text-xs text-gray-500">
|
<div className="mt-2 text-xs text-muted-foreground">
|
||||||
선택된 컬럼: {selectedColumns.length}개 / 전체: {fromColumns.length}개
|
선택된 컬럼: {selectedColumns.length}개 / 전체: {fromColumns.length}개
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 빠른 매핑 버튼들 */}
|
{/* 빠른 매핑 버튼들 */}
|
||||||
{selectedColumns.length > 0 && toApiFields.length > 0 && (
|
{selectedColumns.length > 0 && toApiFields.length > 0 && (
|
||||||
<div className="mt-3 rounded-lg border border-green-200 bg-green-50 p-3">
|
<div className="mt-3 rounded-lg border border-emerald-200 bg-emerald-50 p-3">
|
||||||
<div className="mb-2 text-sm font-medium text-green-800">빠른 매핑</div>
|
<div className="mb-2 text-sm font-medium text-emerald-800">빠른 매핑</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -1051,7 +1051,7 @@ export default function BatchManagementNewPage() {
|
||||||
setDbToApiFieldMapping(mapping);
|
setDbToApiFieldMapping(mapping);
|
||||||
toast.success(`${Object.keys(mapping).length}개 컬럼이 자동 매핑되었습니다.`);
|
toast.success(`${Object.keys(mapping).length}개 컬럼이 자동 매핑되었습니다.`);
|
||||||
}}
|
}}
|
||||||
className="rounded bg-blue-600 px-3 py-1 text-xs text-white hover:bg-blue-700"
|
className="rounded bg-primary px-3 py-1 text-xs text-white hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
스마트 자동 매핑
|
스마트 자동 매핑
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1061,7 +1061,7 @@ export default function BatchManagementNewPage() {
|
||||||
setDbToApiFieldMapping({});
|
setDbToApiFieldMapping({});
|
||||||
toast.success("매핑이 초기화되었습니다.");
|
toast.success("매핑이 초기화되었습니다.");
|
||||||
}}
|
}}
|
||||||
className="rounded bg-gray-600 px-3 py-1 text-xs text-white hover:bg-gray-700"
|
className="rounded bg-foreground/80 px-3 py-1 text-xs text-white hover:bg-foreground/90"
|
||||||
>
|
>
|
||||||
매핑 초기화
|
매핑 초기화
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1071,9 +1071,9 @@ export default function BatchManagementNewPage() {
|
||||||
|
|
||||||
{/* 자동 생성된 JSON 미리보기 */}
|
{/* 자동 생성된 JSON 미리보기 */}
|
||||||
{selectedColumns.length > 0 && (
|
{selectedColumns.length > 0 && (
|
||||||
<div className="mt-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
<div className="mt-3 rounded-lg border border-primary/20 bg-primary/10 p-3">
|
||||||
<div className="mb-2 text-sm font-medium text-blue-800">자동 생성된 JSON 구조</div>
|
<div className="mb-2 text-sm font-medium text-primary">자동 생성된 JSON 구조</div>
|
||||||
<pre className="overflow-x-auto font-mono text-xs text-blue-600">
|
<pre className="overflow-x-auto font-mono text-xs text-primary">
|
||||||
{JSON.stringify(
|
{JSON.stringify(
|
||||||
selectedColumns.reduce(
|
selectedColumns.reduce(
|
||||||
(obj, col) => {
|
(obj, col) => {
|
||||||
|
|
@ -1105,7 +1105,7 @@ export default function BatchManagementNewPage() {
|
||||||
setToApiBody(autoJson);
|
setToApiBody(autoJson);
|
||||||
toast.success("Request Body에 자동 생성된 JSON이 적용되었습니다.");
|
toast.success("Request Body에 자동 생성된 JSON이 적용되었습니다.");
|
||||||
}}
|
}}
|
||||||
className="mt-2 rounded bg-blue-600 px-3 py-1 text-xs text-white hover:bg-blue-700"
|
className="mt-2 rounded bg-primary px-3 py-1 text-xs text-white hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
Request Body에 적용
|
Request Body에 적용
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1231,7 +1231,7 @@ export default function BatchManagementNewPage() {
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
이 컬럼 값이 같으면 UPDATE, 없으면 INSERT 합니다. (예: device_serial_number)
|
이 컬럼 값이 같으면 UPDATE, 없으면 INSERT 합니다. (예: device_serial_number)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1273,7 +1273,7 @@ export default function BatchManagementNewPage() {
|
||||||
placeholder="/api/users"
|
placeholder="/api/users"
|
||||||
/>
|
/>
|
||||||
{(toApiMethod === "PUT" || toApiMethod === "DELETE") && (
|
{(toApiMethod === "PUT" || toApiMethod === "DELETE") && (
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
실제 URL: {toEndpoint}/{urlPathColumn ? `{${urlPathColumn}}` : "{ID}"}
|
실제 URL: {toEndpoint}/{urlPathColumn ? `{${urlPathColumn}}` : "{ID}"}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1310,7 +1310,7 @@ export default function BatchManagementNewPage() {
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
PUT/DELETE 요청 시 URL 경로에 포함될 컬럼을 선택하세요. (예: USER_ID → /api/users/user123)
|
PUT/DELETE 요청 시 URL 경로에 포함될 컬럼을 선택하세요. (예: USER_ID → /api/users/user123)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1321,7 +1321,7 @@ export default function BatchManagementNewPage() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={previewToApiData}
|
onClick={previewToApiData}
|
||||||
className="flex items-center space-x-2 rounded-md bg-green-600 px-4 py-2 text-white hover:bg-green-700"
|
className="flex items-center space-x-2 rounded-md bg-emerald-600 px-4 py-2 text-white hover:bg-green-700"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
<span>API 필드 미리보기</span>
|
<span>API 필드 미리보기</span>
|
||||||
|
|
@ -1330,13 +1330,13 @@ export default function BatchManagementNewPage() {
|
||||||
|
|
||||||
{/* TO API 필드 표시 */}
|
{/* TO API 필드 표시 */}
|
||||||
{toApiFields.length > 0 && (
|
{toApiFields.length > 0 && (
|
||||||
<div className="rounded-lg border border-green-200 bg-green-50 p-3">
|
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3">
|
||||||
<div className="mb-2 text-sm font-medium text-green-800">
|
<div className="mb-2 text-sm font-medium text-emerald-800">
|
||||||
API 필드 목록 ({toApiFields.length}개)
|
API 필드 목록 ({toApiFields.length}개)
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{toApiFields.map((field) => (
|
{toApiFields.map((field) => (
|
||||||
<span key={field} className="rounded bg-green-100 px-2 py-1 text-xs text-green-700">
|
<span key={field} className="rounded bg-emerald-100 px-2 py-1 text-xs text-emerald-700">
|
||||||
{field}
|
{field}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1355,7 +1355,7 @@ export default function BatchManagementNewPage() {
|
||||||
placeholder='{"id": "{{id}}", "name": "{{name}}", "email": "{{email}}"}'
|
placeholder='{"id": "{{id}}", "name": "{{name}}", "email": "{{email}}"}'
|
||||||
className="h-24 w-full rounded-md border p-2 font-mono text-sm"
|
className="h-24 w-full rounded-md border p-2 font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
<div className="mt-1 text-xs text-gray-500">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
DB 컬럼 값을 {"{{컬럼명}}"} 형태로 매핑하세요. 예: {"{{user_id}}, {{user_name}}"}
|
DB 컬럼 값을 {"{{컬럼명}}"} 형태로 매핑하세요. 예: {"{{user_id}}, {{user_name}}"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1363,15 +1363,15 @@ export default function BatchManagementNewPage() {
|
||||||
|
|
||||||
{/* API 호출 정보 */}
|
{/* API 호출 정보 */}
|
||||||
{toApiUrl && toApiKey && toEndpoint && (
|
{toApiUrl && toApiKey && toEndpoint && (
|
||||||
<div className="rounded-lg bg-gray-50 p-3">
|
<div className="rounded-lg bg-muted p-3">
|
||||||
<div className="text-sm font-medium text-gray-700">API 호출 정보</div>
|
<div className="text-sm font-medium text-foreground">API 호출 정보</div>
|
||||||
<div className="mt-1 text-sm text-gray-600">
|
<div className="mt-1 text-sm text-muted-foreground">
|
||||||
{toApiMethod} {toApiUrl}
|
{toApiMethod} {toApiUrl}
|
||||||
{toEndpoint}
|
{toEndpoint}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-500">Headers: X-API-Key: {toApiKey.substring(0, 10)}...</div>
|
<div className="mt-1 text-xs text-muted-foreground">Headers: X-API-Key: {toApiKey.substring(0, 10)}...</div>
|
||||||
{toApiBody && (
|
{toApiBody && (
|
||||||
<div className="mt-1 text-xs text-blue-600">Body: {toApiBody.substring(0, 50)}...</div>
|
<div className="mt-1 text-xs text-primary">Body: {toApiBody.substring(0, 50)}...</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1394,7 +1394,7 @@ export default function BatchManagementNewPage() {
|
||||||
데이터 불러오고 매핑하기
|
데이터 불러오고 매핑하기
|
||||||
</Button>
|
</Button>
|
||||||
{(!fromApiUrl || !fromEndpoint || !toTable) && (
|
{(!fromApiUrl || !fromEndpoint || !toTable) && (
|
||||||
<p className="ml-4 flex items-center text-xs text-gray-500">
|
<p className="ml-4 flex items-center text-xs text-muted-foreground">
|
||||||
FROM 섹션과 TO 섹션의 필수 값을 모두 입력해야 합니다.
|
FROM 섹션과 TO 섹션의 필수 값을 모두 입력해야 합니다.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1693,17 +1693,17 @@ const DbToRestApiMappingCard = memo(function DbToRestApiMappingCard({
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="max-h-96 space-y-3 overflow-y-auto rounded-lg border p-4">
|
<div className="max-h-96 space-y-3 overflow-y-auto rounded-lg border p-4">
|
||||||
{selectedColumnObjects.map((column) => (
|
{selectedColumnObjects.map((column) => (
|
||||||
<div key={column.column_name} className="flex items-center space-x-4 rounded-lg bg-gray-50 p-3">
|
<div key={column.column_name} className="flex items-center space-x-4 rounded-lg bg-muted p-3">
|
||||||
{/* DB 컬럼 정보 */}
|
{/* DB 컬럼 정보 */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-sm font-medium">{column.column_name}</div>
|
<div className="text-sm font-medium">{column.column_name}</div>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-muted-foreground">
|
||||||
타입: {column.data_type} | NULL: {column.is_nullable ? "Y" : "N"}
|
타입: {column.data_type} | NULL: {column.is_nullable ? "Y" : "N"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 화살표 */}
|
{/* 화살표 */}
|
||||||
<div className="text-gray-400">→</div>
|
<div className="text-muted-foreground/70">→</div>
|
||||||
|
|
||||||
{/* API 필드 선택 드롭다운 */}
|
{/* API 필드 선택 드롭다운 */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -1735,7 +1735,7 @@ const DbToRestApiMappingCard = memo(function DbToRestApiMappingCard({
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="API 필드명을 직접 입력하세요"
|
placeholder="API 필드명을 직접 입력하세요"
|
||||||
className="mt-2 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
className="mt-2 w-full rounded-md border border-input px-3 py-2 text-sm focus:ring-2 focus:ring-ring focus:outline-none"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setDbToApiFieldMapping((prev) => ({
|
setDbToApiFieldMapping((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -1745,7 +1745,7 @@ const DbToRestApiMappingCard = memo(function DbToRestApiMappingCard({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-1 text-xs text-gray-500">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
{dbToApiFieldMapping[column.column_name]
|
{dbToApiFieldMapping[column.column_name]
|
||||||
? `매핑: ${column.column_name} → ${dbToApiFieldMapping[column.column_name]}`
|
? `매핑: ${column.column_name} → ${dbToApiFieldMapping[column.column_name]}`
|
||||||
: `기본값: ${column.column_name} (DB 컬럼명 사용)`}
|
: `기본값: ${column.column_name} (DB 컬럼명 사용)`}
|
||||||
|
|
@ -1755,32 +1755,32 @@ const DbToRestApiMappingCard = memo(function DbToRestApiMappingCard({
|
||||||
{/* 템플릿 미리보기 */}
|
{/* 템플릿 미리보기 */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="rounded border bg-white p-2 font-mono text-sm">{`{{${column.column_name}}}`}</div>
|
<div className="rounded border bg-white p-2 font-mono text-sm">{`{{${column.column_name}}}`}</div>
|
||||||
<div className="mt-1 text-xs text-gray-500">실제 DB 값으로 치환됩니다</div>
|
<div className="mt-1 text-xs text-muted-foreground">실제 DB 값으로 치환됩니다</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedColumns.length > 0 && (
|
{selectedColumns.length > 0 && (
|
||||||
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
<div className="mt-4 rounded-lg border border-primary/20 bg-primary/10 p-3">
|
||||||
<div className="text-sm font-medium text-blue-800">자동 생성된 JSON 구조</div>
|
<div className="text-sm font-medium text-primary">자동 생성된 JSON 구조</div>
|
||||||
<pre className="mt-1 overflow-x-auto font-mono text-xs text-blue-600">{autoJsonPreview}</pre>
|
<pre className="mt-1 overflow-x-auto font-mono text-xs text-primary">{autoJsonPreview}</pre>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setToApiBody(autoJsonPreview);
|
setToApiBody(autoJsonPreview);
|
||||||
toast.success("Request Body에 자동 생성된 JSON이 적용되었습니다.");
|
toast.success("Request Body에 자동 생성된 JSON이 적용되었습니다.");
|
||||||
}}
|
}}
|
||||||
className="mt-2 rounded bg-blue-600 px-3 py-1 text-xs text-white hover:bg-blue-700"
|
className="mt-2 rounded bg-primary px-3 py-1 text-xs text-white hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
Request Body에 적용
|
Request Body에 적용
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
<div className="mt-4 rounded-lg border border-primary/20 bg-primary/10 p-3">
|
||||||
<div className="text-sm font-medium text-blue-800">매핑 사용 예시</div>
|
<div className="text-sm font-medium text-primary">매핑 사용 예시</div>
|
||||||
<div className="mt-1 font-mono text-xs text-blue-600">
|
<div className="mt-1 font-mono text-xs text-primary">
|
||||||
{'{"id": "{{id}}", "name": "{{user_name}}", "email": "{{email}}"}'}
|
{'{"id": "{{id}}", "name": "{{user_name}}", "email": "{{email}}"}'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,7 @@ export default function AutoFillTab() {
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onClick={() => handleDeleteConfirm(group.groupCode)}>
|
<Button variant="ghost" size="icon" onClick={() => handleDeleteConfirm(group.groupCode)}>
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -676,7 +676,7 @@ export default function AutoFillTab() {
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
<AlertDialogAction onClick={handleDelete} className="bg-destructive hover:bg-destructive">
|
||||||
삭제
|
삭제
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -577,9 +577,9 @@ export default function CascadingRelationsTab() {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<span className="rounded bg-blue-100 px-2 py-0.5 text-blue-700">{relation.parent_table}</span>
|
<span className="rounded bg-primary/10 px-2 py-0.5 text-primary">{relation.parent_table}</span>
|
||||||
<ChevronRight className="text-muted-foreground h-4 w-4" />
|
<ChevronRight className="text-muted-foreground h-4 w-4" />
|
||||||
<span className="rounded bg-green-100 px-2 py-0.5 text-green-700">
|
<span className="rounded bg-emerald-100 px-2 py-0.5 text-emerald-700">
|
||||||
{relation.child_table}
|
{relation.child_table}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -619,7 +619,7 @@ export default function CascadingRelationsTab() {
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Step 1: 부모 테이블 */}
|
{/* Step 1: 부모 테이블 */}
|
||||||
<div className="rounded-lg border p-4">
|
<div className="rounded-lg border p-4">
|
||||||
<h4 className="mb-3 text-sm font-semibold text-blue-600">1. 부모 (상위 선택)</h4>
|
<h4 className="mb-3 text-sm font-semibold text-primary">1. 부모 (상위 선택)</h4>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs">테이블</Label>
|
<Label className="text-xs">테이블</Label>
|
||||||
|
|
@ -696,7 +696,7 @@ export default function CascadingRelationsTab() {
|
||||||
|
|
||||||
{/* Step 2: 자식 테이블 */}
|
{/* Step 2: 자식 테이블 */}
|
||||||
<div className="rounded-lg border p-4">
|
<div className="rounded-lg border p-4">
|
||||||
<h4 className="mb-3 text-sm font-semibold text-green-600">2. 자식 (하위 옵션)</h4>
|
<h4 className="mb-3 text-sm font-semibold text-emerald-600">2. 자식 (하위 옵션)</h4>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs">테이블</Label>
|
<Label className="text-xs">테이블</Label>
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ export default function ConditionTab() {
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<span className="text-muted-foreground">{condition.conditionField}</span>
|
<span className="text-muted-foreground">{condition.conditionField}</span>
|
||||||
<span className="mx-1 text-blue-600">{getOperatorLabel(condition.conditionOperator)}</span>
|
<span className="mx-1 text-primary">{getOperatorLabel(condition.conditionOperator)}</span>
|
||||||
<span className="font-medium">{condition.conditionValue}</span>
|
<span className="font-medium">{condition.conditionValue}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -329,7 +329,7 @@ export default function ConditionTab() {
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleDeleteConfirm(condition.conditionId!)}
|
onClick={() => handleDeleteConfirm(condition.conditionId!)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -491,7 +491,7 @@ export default function ConditionTab() {
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
<AlertDialogAction onClick={handleDelete} className="bg-destructive hover:bg-destructive">
|
||||||
삭제
|
삭제
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -501,7 +501,7 @@ export default function HierarchyTab() {
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onClick={() => handleDeleteConfirm(group.groupCode)}>
|
<Button variant="ghost" size="icon" onClick={() => handleDeleteConfirm(group.groupCode)}>
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -548,7 +548,7 @@ export default function HierarchyTab() {
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleDeleteLevel(level.levelId!, group.groupCode)}
|
onClick={() => handleDeleteLevel(level.levelId!, group.groupCode)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -836,7 +836,7 @@ export default function HierarchyTab() {
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
<AlertDialogAction onClick={handleDelete} className="bg-destructive hover:bg-destructive">
|
||||||
삭제
|
삭제
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,7 @@ export default function MutualExclusionTab() {
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onClick={() => handleDeleteConfirm(exclusion.exclusionId!)}>
|
<Button variant="ghost" size="icon" onClick={() => handleDeleteConfirm(exclusion.exclusionId!)}>
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -404,7 +404,7 @@ export default function MutualExclusionTab() {
|
||||||
/>
|
/>
|
||||||
{fieldList.length > 2 && (
|
{fieldList.length > 2 && (
|
||||||
<Button variant="ghost" size="icon" onClick={() => removeField(index)}>
|
<Button variant="ghost" size="icon" onClick={() => removeField(index)}>
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -571,7 +571,7 @@ export default function MutualExclusionTab() {
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
<AlertDialogAction onClick={handleDelete} className="bg-destructive hover:bg-destructive">
|
||||||
삭제
|
삭제
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ export default function DebugLayoutPage() {
|
||||||
<h1 className="mb-4 text-2xl font-bold">관리자 레이아웃 디버깅</h1>
|
<h1 className="mb-4 text-2xl font-bold">관리자 레이아웃 디버깅</h1>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded bg-green-100 p-4">
|
<div className="rounded bg-emerald-100 p-4">
|
||||||
<h2 className="mb-2 font-semibold">토큰 상태</h2>
|
<h2 className="mb-2 font-semibold">토큰 상태</h2>
|
||||||
<p>토큰 존재: {debugInfo.hasToken ? "✅ 예" : "❌ 아니오"}</p>
|
<p>토큰 존재: {debugInfo.hasToken ? "✅ 예" : "❌ 아니오"}</p>
|
||||||
<p>토큰 길이: {debugInfo.tokenLength}</p>
|
<p>토큰 길이: {debugInfo.tokenLength}</p>
|
||||||
|
|
@ -142,14 +142,14 @@ export default function DebugLayoutPage() {
|
||||||
<p>SessionStorage 토큰: {debugInfo.sessionToken ? "✅ 존재" : "❌ 없음"}</p>
|
<p>SessionStorage 토큰: {debugInfo.sessionToken ? "✅ 존재" : "❌ 없음"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded bg-blue-100 p-4">
|
<div className="rounded bg-primary/10 p-4">
|
||||||
<h2 className="mb-2 font-semibold">페이지 정보</h2>
|
<h2 className="mb-2 font-semibold">페이지 정보</h2>
|
||||||
<p>현재 URL: {debugInfo.currentUrl}</p>
|
<p>현재 URL: {debugInfo.currentUrl}</p>
|
||||||
<p>Pathname: {debugInfo.pathname}</p>
|
<p>Pathname: {debugInfo.pathname}</p>
|
||||||
<p>시간: {debugInfo.timestamp}</p>
|
<p>시간: {debugInfo.timestamp}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded bg-yellow-100 p-4">
|
<div className="rounded bg-amber-100 p-4">
|
||||||
<h2 className="mb-2 font-semibold">토큰 관리</h2>
|
<h2 className="mb-2 font-semibold">토큰 관리</h2>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -157,11 +157,11 @@ export default function DebugLayoutPage() {
|
||||||
const token = localStorage.getItem("authToken");
|
const token = localStorage.getItem("authToken");
|
||||||
alert(`토큰: ${token ? "존재" : "없음"}\n길이: ${token ? token.length : 0}`);
|
alert(`토큰: ${token ? "존재" : "없음"}\n길이: ${token ? token.length : 0}`);
|
||||||
}}
|
}}
|
||||||
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
className="rounded bg-primary px-4 py-2 text-white hover:bg-primary"
|
||||||
>
|
>
|
||||||
토큰 확인
|
토큰 확인
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleTokenSync} className="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600">
|
<button onClick={handleTokenSync} className="rounded bg-emerald-500 px-4 py-2 text-white hover:bg-emerald-600">
|
||||||
토큰 동기화
|
토큰 동기화
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
@ -173,13 +173,13 @@ export default function DebugLayoutPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded bg-orange-100 p-4">
|
<div className="rounded bg-amber-100 p-4">
|
||||||
<h2 className="mb-2 font-semibold">API 테스트</h2>
|
<h2 className="mb-2 font-semibold">API 테스트</h2>
|
||||||
<div className="mb-4 space-x-2">
|
<div className="mb-4 space-x-2">
|
||||||
<button onClick={handleApiTest} className="rounded bg-orange-500 px-4 py-2 text-white hover:bg-orange-600">
|
<button onClick={handleApiTest} className="rounded bg-amber-500 px-4 py-2 text-white hover:bg-orange-600">
|
||||||
인증 상태 API 테스트
|
인증 상태 API 테스트
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleUserApiTest} className="rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600">
|
<button onClick={handleUserApiTest} className="rounded bg-destructive px-4 py-2 text-white hover:bg-destructive">
|
||||||
사용자 API 테스트
|
사용자 API 테스트
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
@ -194,7 +194,7 @@ export default function DebugLayoutPage() {
|
||||||
<div
|
<div
|
||||||
className={`rounded p-3 ${
|
className={`rounded p-3 ${
|
||||||
apiTestResult.status === "success"
|
apiTestResult.status === "success"
|
||||||
? "bg-green-200"
|
? "bg-emerald-200"
|
||||||
: apiTestResult.status === "error"
|
: apiTestResult.status === "error"
|
||||||
? "bg-red-200"
|
? "bg-red-200"
|
||||||
: "bg-yellow-200"
|
: "bg-yellow-200"
|
||||||
|
|
|
||||||
|
|
@ -28,27 +28,27 @@ export default function SimpleDebugPage() {
|
||||||
<h1 className="mb-4 text-2xl font-bold">간단한 토큰 디버깅</h1>
|
<h1 className="mb-4 text-2xl font-bold">간단한 토큰 디버깅</h1>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded bg-green-100 p-4">
|
<div className="rounded bg-emerald-100 p-4">
|
||||||
<h2 className="mb-2 font-semibold">토큰 상태</h2>
|
<h2 className="mb-2 font-semibold">토큰 상태</h2>
|
||||||
<p>토큰 존재: {tokenInfo.hasToken ? "✅ 예" : "❌ 아니오"}</p>
|
<p>토큰 존재: {tokenInfo.hasToken ? "✅ 예" : "❌ 아니오"}</p>
|
||||||
<p>토큰 길이: {tokenInfo.tokenLength}</p>
|
<p>토큰 길이: {tokenInfo.tokenLength}</p>
|
||||||
<p>토큰 시작: {tokenInfo.tokenStart}</p>
|
<p>토큰 시작: {tokenInfo.tokenStart}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded bg-blue-100 p-4">
|
<div className="rounded bg-primary/10 p-4">
|
||||||
<h2 className="mb-2 font-semibold">페이지 정보</h2>
|
<h2 className="mb-2 font-semibold">페이지 정보</h2>
|
||||||
<p>현재 URL: {tokenInfo.currentUrl}</p>
|
<p>현재 URL: {tokenInfo.currentUrl}</p>
|
||||||
<p>시간: {tokenInfo.timestamp}</p>
|
<p>시간: {tokenInfo.timestamp}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded bg-yellow-100 p-4">
|
<div className="rounded bg-amber-100 p-4">
|
||||||
<h2 className="mb-2 font-semibold">테스트 버튼</h2>
|
<h2 className="mb-2 font-semibold">테스트 버튼</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const token = localStorage.getItem("authToken");
|
const token = localStorage.getItem("authToken");
|
||||||
alert(`토큰: ${token ? "존재" : "없음"}\n길이: ${token ? token.length : 0}`);
|
alert(`토큰: ${token ? "존재" : "없음"}\n길이: ${token ? token.length : 0}`);
|
||||||
}}
|
}}
|
||||||
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
className="rounded bg-primary px-4 py-2 text-white hover:bg-primary"
|
||||||
>
|
>
|
||||||
토큰 확인
|
토큰 확인
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,16 @@ export default function AdminDebugPage() {
|
||||||
<h1 className="mb-4 text-2xl font-bold">어드민 권한 디버깅</h1>
|
<h1 className="mb-4 text-2xl font-bold">어드민 권한 디버깅</h1>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded bg-gray-100 p-4">
|
<div className="rounded bg-muted p-4">
|
||||||
<h2 className="mb-2 font-semibold">인증 상태</h2>
|
<h2 className="mb-2 font-semibold">인증 상태</h2>
|
||||||
<p>로딩: {loading ? "예" : "아니오"}</p>
|
<p>로딩: {loading ? "예" : "아니오"}</p>
|
||||||
<p>로그인: {isLoggedIn ? "예" : "아니오"}</p>
|
<p>로그인: {isLoggedIn ? "예" : "아니오"}</p>
|
||||||
<p>관리자: {isAdmin ? "예" : "아니오"}</p>
|
<p>관리자: {isAdmin ? "예" : "아니오"}</p>
|
||||||
{error && <p className="text-red-500">에러: {error}</p>}
|
{error && <p className="text-destructive">에러: {error}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<div className="rounded bg-blue-100 p-4">
|
<div className="rounded bg-primary/10 p-4">
|
||||||
<h2 className="mb-2 font-semibold">사용자 정보</h2>
|
<h2 className="mb-2 font-semibold">사용자 정보</h2>
|
||||||
<p>ID: {user.userId}</p>
|
<p>ID: {user.userId}</p>
|
||||||
<p>이름: {user.userName}</p>
|
<p>이름: {user.userName}</p>
|
||||||
|
|
@ -34,7 +34,7 @@ export default function AdminDebugPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rounded bg-green-100 p-4">
|
<div className="rounded bg-emerald-100 p-4">
|
||||||
<h2 className="mb-2 font-semibold">토큰 정보</h2>
|
<h2 className="mb-2 font-semibold">토큰 정보</h2>
|
||||||
<p>
|
<p>
|
||||||
localStorage 토큰: {typeof window !== "undefined" && localStorage.getItem("authToken") ? "존재" : "없음"}
|
localStorage 토큰: {typeof window !== "undefined" && localStorage.getItem("authToken") ? "존재" : "없음"}
|
||||||
|
|
|
||||||
|
|
@ -220,13 +220,13 @@ export default function LayoutManagementPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||||
{/* 페이지 제목 */}
|
{/* 페이지 제목 */}
|
||||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">레이아웃 관리</h1>
|
<h1 className="text-3xl font-bold text-foreground">레이아웃 관리</h1>
|
||||||
<p className="mt-2 text-gray-600">화면 레이아웃을 생성하고 관리합니다</p>
|
<p className="mt-2 text-muted-foreground">화면 레이아웃을 생성하고 관리합니다</p>
|
||||||
</div>
|
</div>
|
||||||
<Button className="flex items-center gap-2 shadow-sm" onClick={() => setCreateModalOpen(true)}>
|
<Button className="flex items-center gap-2 shadow-sm" onClick={() => setCreateModalOpen(true)}>
|
||||||
<Plus className="h-4 w-4" />새 레이아웃
|
<Plus className="h-4 w-4" />새 레이아웃
|
||||||
|
|
@ -239,7 +239,7 @@ export default function LayoutManagementPage() {
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-muted-foreground/70" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="레이아웃 이름 또는 설명으로 검색..."
|
placeholder="레이아웃 이름 또는 설명으로 검색..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
|
|
@ -276,7 +276,7 @@ export default function LayoutManagementPage() {
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="py-8 text-center">로딩 중...</div>
|
<div className="py-8 text-center">로딩 중...</div>
|
||||||
) : layouts.length === 0 ? (
|
) : layouts.length === 0 ? (
|
||||||
<div className="py-8 text-center text-gray-500">레이아웃이 없습니다.</div>
|
<div className="py-8 text-center text-muted-foreground">레이아웃이 없습니다.</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* 레이아웃 그리드 */}
|
{/* 레이아웃 그리드 */}
|
||||||
|
|
@ -288,7 +288,7 @@ export default function LayoutManagementPage() {
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CategoryIcon className="h-5 w-5 text-gray-600" />
|
<CategoryIcon className="h-5 w-5 text-muted-foreground" />
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
{CATEGORY_NAMES[layout.category as keyof typeof CATEGORY_NAMES]}
|
{CATEGORY_NAMES[layout.category as keyof typeof CATEGORY_NAMES]}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -312,7 +312,7 @@ export default function LayoutManagementPage() {
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
복제
|
복제
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleDelete(layout)} className="text-red-600">
|
<DropdownMenuItem onClick={() => handleDelete(layout)} className="text-destructive">
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
삭제
|
삭제
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -321,17 +321,17 @@ export default function LayoutManagementPage() {
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-lg">{layout.layoutName}</CardTitle>
|
<CardTitle className="text-lg">{layout.layoutName}</CardTitle>
|
||||||
{layout.description && (
|
{layout.description && (
|
||||||
<p className="line-clamp-2 text-sm text-gray-600">{layout.description}</p>
|
<p className="line-clamp-2 text-sm text-muted-foreground">{layout.description}</p>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-gray-500">타입:</span>
|
<span className="text-muted-foreground">타입:</span>
|
||||||
<Badge variant="outline">{layout.layoutType}</Badge>
|
<Badge variant="outline">{layout.layoutType}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-gray-500">존 개수:</span>
|
<span className="text-muted-foreground">존 개수:</span>
|
||||||
<span>{layout.zonesConfig.length}개</span>
|
<span>{layout.zonesConfig.length}개</span>
|
||||||
</div>
|
</div>
|
||||||
{layout.isPublic === "Y" && (
|
{layout.isPublic === "Y" && (
|
||||||
|
|
@ -397,7 +397,7 @@ export default function LayoutManagementPage() {
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={confirmDelete} className="bg-red-600 hover:bg-red-700">
|
<AlertDialogAction onClick={confirmDelete} className="bg-destructive hover:bg-red-700">
|
||||||
삭제
|
삭제
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -59,25 +59,25 @@ export default function MonitoringPage() {
|
||||||
const getStatusIcon = (status: string) => {
|
const getStatusIcon = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
return <CheckCircle className="h-4 w-4 text-emerald-500" />;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return <AlertCircle className="h-4 w-4 text-red-500" />;
|
return <AlertCircle className="h-4 w-4 text-destructive" />;
|
||||||
case 'running':
|
case 'running':
|
||||||
return <Play className="h-4 w-4 text-blue-500" />;
|
return <Play className="h-4 w-4 text-primary" />;
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return <Clock className="h-4 w-4 text-yellow-500" />;
|
return <Clock className="h-4 w-4 text-amber-500" />;
|
||||||
default:
|
default:
|
||||||
return <Clock className="h-4 w-4 text-gray-500" />;
|
return <Clock className="h-4 w-4 text-muted-foreground" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
const getStatusBadge = (status: string) => {
|
||||||
const variants = {
|
const variants = {
|
||||||
completed: "bg-green-100 text-green-800",
|
completed: "bg-emerald-100 text-emerald-800",
|
||||||
failed: "bg-destructive/20 text-red-800",
|
failed: "bg-destructive/20 text-red-800",
|
||||||
running: "bg-primary/20 text-blue-800",
|
running: "bg-primary/20 text-primary",
|
||||||
pending: "bg-yellow-100 text-yellow-800",
|
pending: "bg-amber-100 text-yellow-800",
|
||||||
cancelled: "bg-gray-100 text-gray-800",
|
cancelled: "bg-muted text-foreground",
|
||||||
};
|
};
|
||||||
|
|
||||||
const labels = {
|
const labels = {
|
||||||
|
|
@ -120,7 +120,7 @@ export default function MonitoringPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -191,7 +191,7 @@ export default function MonitoringPage() {
|
||||||
<div className="text-2xl">✅</div>
|
<div className="text-2xl">✅</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-green-600">{monitoring.successful_jobs_today}</div>
|
<div className="text-2xl font-bold text-emerald-600">{monitoring.successful_jobs_today}</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
성공률: {getSuccessRate()}%
|
성공률: {getSuccessRate()}%
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export default function BarcodeLabelDesignerPage() {
|
||||||
return (
|
return (
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<BarcodeDesignerProvider labelId={labelId}>
|
<BarcodeDesignerProvider labelId={labelId}>
|
||||||
<div className="flex h-screen flex-col overflow-hidden bg-gray-50">
|
<div className="flex h-screen flex-col overflow-hidden bg-muted">
|
||||||
<BarcodeDesignerToolbar />
|
<BarcodeDesignerToolbar />
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<BarcodeDesignerLeftPanel />
|
<BarcodeDesignerLeftPanel />
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,12 @@ export default function BarcodeLabelManagementPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none space-y-8 px-4 py-8">
|
<div className="w-full max-w-none space-y-8 px-4 py-8">
|
||||||
<div className="flex items-center justify-between rounded-lg border bg-white p-6 shadow-sm">
|
<div className="flex items-center justify-between rounded-lg border bg-white p-6 shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">바코드 라벨 관리</h1>
|
<h1 className="text-3xl font-bold text-foreground">바코드 라벨 관리</h1>
|
||||||
<p className="mt-2 text-gray-600">ZD421 등 바코드 프린터용 라벨을 작성하고 출력합니다</p>
|
<p className="mt-2 text-muted-foreground">ZD421 등 바코드 프린터용 라벨을 작성하고 출력합니다</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleCreateNew} className="gap-2">
|
<Button onClick={handleCreateNew} className="gap-2">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
|
|
@ -43,7 +43,7 @@ export default function BarcodeLabelManagementPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="shadow-sm">
|
<Card className="shadow-sm">
|
||||||
<CardHeader className="bg-gray-50/50">
|
<CardHeader className="bg-muted/50">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Search className="h-5 w-5" />
|
<Search className="h-5 w-5" />
|
||||||
검색
|
검색
|
||||||
|
|
@ -75,7 +75,7 @@ export default function BarcodeLabelManagementPage() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="shadow-sm">
|
<Card className="shadow-sm">
|
||||||
<CardHeader className="bg-gray-50/50">
|
<CardHeader className="bg-muted/50">
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
바코드 라벨 목록
|
바코드 라벨 목록
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export default function ReportDesignerPage() {
|
||||||
return (
|
return (
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<ReportDesignerProvider reportId={reportId}>
|
<ReportDesignerProvider reportId={reportId}>
|
||||||
<div className="flex h-screen flex-col overflow-hidden bg-gray-50">
|
<div className="flex h-screen flex-col overflow-hidden bg-muted">
|
||||||
{/* 상단 툴바 */}
|
{/* 상단 툴바 */}
|
||||||
<ReportDesignerToolbar />
|
<ReportDesignerToolbar />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,13 @@ export default function ReportManagementPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none space-y-8 px-4 py-8">
|
<div className="w-full max-w-none space-y-8 px-4 py-8">
|
||||||
{/* 페이지 제목 */}
|
{/* 페이지 제목 */}
|
||||||
<div className="flex items-center justify-between rounded-lg border bg-white p-6 shadow-sm">
|
<div className="flex items-center justify-between rounded-lg border bg-white p-6 shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">리포트 관리</h1>
|
<h1 className="text-3xl font-bold text-foreground">리포트 관리</h1>
|
||||||
<p className="mt-2 text-gray-600">리포트를 생성하고 관리합니다</p>
|
<p className="mt-2 text-muted-foreground">리포트를 생성하고 관리합니다</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleCreateNew} className="gap-2">
|
<Button onClick={handleCreateNew} className="gap-2">
|
||||||
<Plus className="h-4 w-4" />새 리포트
|
<Plus className="h-4 w-4" />새 리포트
|
||||||
|
|
@ -45,7 +45,7 @@ export default function ReportManagementPage() {
|
||||||
|
|
||||||
{/* 검색 영역 */}
|
{/* 검색 영역 */}
|
||||||
<Card className="shadow-sm">
|
<Card className="shadow-sm">
|
||||||
<CardHeader className="bg-gray-50/50">
|
<CardHeader className="bg-muted/50">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Search className="h-5 w-5" />
|
<Search className="h-5 w-5" />
|
||||||
검색
|
검색
|
||||||
|
|
@ -78,7 +78,7 @@ export default function ReportManagementPage() {
|
||||||
|
|
||||||
{/* 리포트 목록 */}
|
{/* 리포트 목록 */}
|
||||||
<Card className="shadow-sm">
|
<Card className="shadow-sm">
|
||||||
<CardHeader className="bg-gray-50/50">
|
<CardHeader className="bg-muted/50">
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
📋 리포트 목록
|
📋 리포트 목록
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ export default function EditWebTypePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="mb-6 flex items-center gap-4">
|
<div className="mb-6 flex items-center gap-4">
|
||||||
|
|
@ -244,7 +244,7 @@ export default function EditWebTypePage() {
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="type_name">
|
<Label htmlFor="type_name">
|
||||||
웹타입명 <span className="text-red-500">*</span>
|
웹타입명 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="type_name"
|
id="type_name"
|
||||||
|
|
@ -267,7 +267,7 @@ export default function EditWebTypePage() {
|
||||||
{/* 카테고리 */}
|
{/* 카테고리 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="category">
|
<Label htmlFor="category">
|
||||||
카테고리 <span className="text-red-500">*</span>
|
카테고리 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={formData.category || ""} onValueChange={(value) => handleInputChange("category", value)}>
|
<Select value={formData.category || ""} onValueChange={(value) => handleInputChange("category", value)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -286,7 +286,7 @@ export default function EditWebTypePage() {
|
||||||
{/* 연결된 컴포넌트 */}
|
{/* 연결된 컴포넌트 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="component_name">
|
<Label htmlFor="component_name">
|
||||||
연결된 컴포넌트 <span className="text-red-500">*</span>
|
연결된 컴포넌트 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.component_name || "TextWidget"}
|
value={formData.component_name || "TextWidget"}
|
||||||
|
|
@ -338,15 +338,15 @@ export default function EditWebTypePage() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{formData.config_panel && (
|
{formData.config_panel && (
|
||||||
<div className="rounded-lg border border-green-200 bg-green-50 p-3">
|
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
<div className="h-2 w-2 rounded-full bg-emerald-500"></div>
|
||||||
<span className="text-sm font-medium text-green-700">
|
<span className="text-sm font-medium text-emerald-700">
|
||||||
현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel}
|
현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{getConfigPanelInfo(formData.config_panel)?.description && (
|
{getConfigPanelInfo(formData.config_panel)?.description && (
|
||||||
<p className="mt-1 ml-4 text-xs text-green-600">
|
<p className="mt-1 ml-4 text-xs text-emerald-600">
|
||||||
{getConfigPanelInfo(formData.config_panel)?.description}
|
{getConfigPanelInfo(formData.config_panel)?.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -427,7 +427,7 @@ export default function EditWebTypePage() {
|
||||||
rows={4}
|
rows={4}
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
{jsonErrors.default_config && <p className="text-xs text-red-500">{jsonErrors.default_config}</p>}
|
{jsonErrors.default_config && <p className="text-xs text-destructive">{jsonErrors.default_config}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 유효성 검사 규칙 */}
|
{/* 유효성 검사 규칙 */}
|
||||||
|
|
@ -441,7 +441,7 @@ export default function EditWebTypePage() {
|
||||||
rows={4}
|
rows={4}
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
{jsonErrors.validation_rules && <p className="text-xs text-red-500">{jsonErrors.validation_rules}</p>}
|
{jsonErrors.validation_rules && <p className="text-xs text-destructive">{jsonErrors.validation_rules}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 기본 스타일 */}
|
{/* 기본 스타일 */}
|
||||||
|
|
@ -455,7 +455,7 @@ export default function EditWebTypePage() {
|
||||||
rows={4}
|
rows={4}
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
{jsonErrors.default_style && <p className="text-xs text-red-500">{jsonErrors.default_style}</p>}
|
{jsonErrors.default_style && <p className="text-xs text-destructive">{jsonErrors.default_style}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 입력 속성 */}
|
{/* 입력 속성 */}
|
||||||
|
|
@ -469,7 +469,7 @@ export default function EditWebTypePage() {
|
||||||
rows={4}
|
rows={4}
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
{jsonErrors.input_properties && <p className="text-xs text-red-500">{jsonErrors.input_properties}</p>}
|
{jsonErrors.input_properties && <p className="text-xs text-destructive">{jsonErrors.input_properties}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
@ -498,8 +498,8 @@ export default function EditWebTypePage() {
|
||||||
|
|
||||||
{/* 에러 메시지 */}
|
{/* 에러 메시지 */}
|
||||||
{updateError && (
|
{updateError && (
|
||||||
<div className="mt-4 rounded-md border border-red-200 bg-red-50 p-4">
|
<div className="mt-4 rounded-md border border-destructive/20 bg-destructive/10 p-4">
|
||||||
<p className="text-red-600">
|
<p className="text-destructive">
|
||||||
수정 중 오류가 발생했습니다: {updateError instanceof Error ? updateError.message : "알 수 없는 오류"}
|
수정 중 오류가 발생했습니다: {updateError instanceof Error ? updateError.message : "알 수 없는 오류"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export default function WebTypeDetailPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-96 items-center justify-center">
|
<div className="flex h-96 items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-2 text-lg text-red-600">웹타입 정보를 불러오는데 실패했습니다.</div>
|
<div className="mb-2 text-lg text-destructive">웹타입 정보를 불러오는데 실패했습니다.</div>
|
||||||
<Link href="/admin/standards">
|
<Link href="/admin/standards">
|
||||||
<Button variant="outline">목록으로 돌아가기</Button>
|
<Button variant="outline">목록으로 돌아가기</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -80,7 +80,7 @@ export default function WebTypeDetailPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ export default function NewWebTypePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="mb-6 flex items-center gap-4">
|
<div className="mb-6 flex items-center gap-4">
|
||||||
|
|
@ -186,7 +186,7 @@ export default function NewWebTypePage() {
|
||||||
{/* 웹타입 코드 */}
|
{/* 웹타입 코드 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="web_type">
|
<Label htmlFor="web_type">
|
||||||
웹타입 코드 <span className="text-red-500">*</span>
|
웹타입 코드 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="web_type"
|
id="web_type"
|
||||||
|
|
@ -202,7 +202,7 @@ export default function NewWebTypePage() {
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="type_name">
|
<Label htmlFor="type_name">
|
||||||
웹타입명 <span className="text-red-500">*</span>
|
웹타입명 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="type_name"
|
id="type_name"
|
||||||
|
|
@ -225,7 +225,7 @@ export default function NewWebTypePage() {
|
||||||
{/* 카테고리 */}
|
{/* 카테고리 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="category">
|
<Label htmlFor="category">
|
||||||
카테고리 <span className="text-red-500">*</span>
|
카테고리 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={formData.category} onValueChange={(value) => handleInputChange("category", value)}>
|
<Select value={formData.category} onValueChange={(value) => handleInputChange("category", value)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -244,7 +244,7 @@ export default function NewWebTypePage() {
|
||||||
{/* 연결된 컴포넌트 */}
|
{/* 연결된 컴포넌트 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="component_name">
|
<Label htmlFor="component_name">
|
||||||
연결된 컴포넌트 <span className="text-red-500">*</span>
|
연결된 컴포넌트 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.component_name || "TextWidget"}
|
value={formData.component_name || "TextWidget"}
|
||||||
|
|
@ -296,15 +296,15 @@ export default function NewWebTypePage() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{formData.config_panel && (
|
{formData.config_panel && (
|
||||||
<div className="rounded-lg border border-green-200 bg-green-50 p-3">
|
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
<div className="h-2 w-2 rounded-full bg-emerald-500"></div>
|
||||||
<span className="text-sm font-medium text-green-700">
|
<span className="text-sm font-medium text-emerald-700">
|
||||||
현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel}
|
현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{getConfigPanelInfo(formData.config_panel)?.description && (
|
{getConfigPanelInfo(formData.config_panel)?.description && (
|
||||||
<p className="mt-1 ml-4 text-xs text-green-600">
|
<p className="mt-1 ml-4 text-xs text-emerald-600">
|
||||||
{getConfigPanelInfo(formData.config_panel)?.description}
|
{getConfigPanelInfo(formData.config_panel)?.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -385,7 +385,7 @@ export default function NewWebTypePage() {
|
||||||
rows={4}
|
rows={4}
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
{jsonErrors.default_config && <p className="text-xs text-red-500">{jsonErrors.default_config}</p>}
|
{jsonErrors.default_config && <p className="text-xs text-destructive">{jsonErrors.default_config}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 유효성 검사 규칙 */}
|
{/* 유효성 검사 규칙 */}
|
||||||
|
|
@ -399,7 +399,7 @@ export default function NewWebTypePage() {
|
||||||
rows={4}
|
rows={4}
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
{jsonErrors.validation_rules && <p className="text-xs text-red-500">{jsonErrors.validation_rules}</p>}
|
{jsonErrors.validation_rules && <p className="text-xs text-destructive">{jsonErrors.validation_rules}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 기본 스타일 */}
|
{/* 기본 스타일 */}
|
||||||
|
|
@ -413,7 +413,7 @@ export default function NewWebTypePage() {
|
||||||
rows={4}
|
rows={4}
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
{jsonErrors.default_style && <p className="text-xs text-red-500">{jsonErrors.default_style}</p>}
|
{jsonErrors.default_style && <p className="text-xs text-destructive">{jsonErrors.default_style}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 입력 속성 */}
|
{/* 입력 속성 */}
|
||||||
|
|
@ -427,7 +427,7 @@ export default function NewWebTypePage() {
|
||||||
rows={4}
|
rows={4}
|
||||||
className="font-mono text-xs"
|
className="font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
{jsonErrors.input_properties && <p className="text-xs text-red-500">{jsonErrors.input_properties}</p>}
|
{jsonErrors.input_properties && <p className="text-xs text-destructive">{jsonErrors.input_properties}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
@ -448,8 +448,8 @@ export default function NewWebTypePage() {
|
||||||
|
|
||||||
{/* 에러 메시지 */}
|
{/* 에러 메시지 */}
|
||||||
{createError && (
|
{createError && (
|
||||||
<div className="mt-4 rounded-md border border-red-200 bg-red-50 p-4">
|
<div className="mt-4 rounded-md border border-destructive/20 bg-destructive/10 p-4">
|
||||||
<p className="text-red-600">
|
<p className="text-destructive">
|
||||||
생성 중 오류가 발생했습니다: {createError instanceof Error ? createError.message : "알 수 없는 오류"}
|
생성 중 오류가 발생했습니다: {createError instanceof Error ? createError.message : "알 수 없는 오류"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -142,28 +142,28 @@ export default function CollectionManagementPage() {
|
||||||
|
|
||||||
const getStatusBadge = (isActive: string) => {
|
const getStatusBadge = (isActive: string) => {
|
||||||
return isActive === "Y" ? (
|
return isActive === "Y" ? (
|
||||||
<Badge className="bg-green-100 text-green-800">활성</Badge>
|
<Badge className="bg-emerald-100 text-emerald-800">활성</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge className="bg-red-100 text-red-800">비활성</Badge>
|
<Badge className="bg-destructive/10 text-red-800">비활성</Badge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTypeBadge = (type: string) => {
|
const getTypeBadge = (type: string) => {
|
||||||
const option = collectionTypeOptions.find(opt => opt.value === type);
|
const option = collectionTypeOptions.find(opt => opt.value === type);
|
||||||
const colors = {
|
const colors = {
|
||||||
full: "bg-blue-100 text-blue-800",
|
full: "bg-primary/10 text-primary",
|
||||||
incremental: "bg-purple-100 text-purple-800",
|
incremental: "bg-purple-100 text-purple-800",
|
||||||
delta: "bg-orange-100 text-orange-800",
|
delta: "bg-amber-100 text-orange-800",
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Badge className={colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800"}>
|
<Badge className={colors[type as keyof typeof colors] || "bg-muted text-foreground"}>
|
||||||
{option?.label || type}
|
{option?.label || type}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@ export default function DataFlowEditPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-64 items-center justify-center">
|
<div className="flex h-64 items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-primary"></div>
|
||||||
<p className="text-gray-500">관계도 정보를 불러오는 중...</p>
|
<p className="text-muted-foreground">관계도 정보를 불러오는 중...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -68,16 +68,16 @@ export default function DataFlowEditPage() {
|
||||||
<span>목록으로</span>
|
<span>목록으로</span>
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">📊 관계도 편집</h1>
|
<h1 className="text-2xl font-bold text-foreground">📊 관계도 편집</h1>
|
||||||
<p className="mt-1 text-gray-600">
|
<p className="mt-1 text-muted-foreground">
|
||||||
<span className="font-medium text-blue-600">{diagramName}</span> 관계도를 편집하고 있습니다
|
<span className="font-medium text-primary">{diagramName}</span> 관계도를 편집하고 있습니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 데이터플로우 디자이너 */}
|
{/* 데이터플로우 디자이너 */}
|
||||||
<div className="rounded-lg border border-gray-200 bg-white">
|
<div className="rounded-lg border border-border bg-white">
|
||||||
<DataFlowDesigner
|
<DataFlowDesigner
|
||||||
key={diagramId}
|
key={diagramId}
|
||||||
selectedDiagram={diagramName}
|
selectedDiagram={diagramName}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export default function NodeEditorPage() {
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center bg-gray-50">
|
<div className="flex h-screen items-center justify-center bg-muted">
|
||||||
<div className="text-gray-500">제어 관리 페이지로 이동중...</div>
|
<div className="text-muted-foreground">제어 관리 페이지로 이동중...</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -528,14 +528,14 @@ export default function I18nPage() {
|
||||||
? "공통"
|
? "공통"
|
||||||
: companies.find((c) => c.code === row.original.companyCode)?.name || row.original.companyCode;
|
: companies.find((c) => c.code === row.original.companyCode)?.name || row.original.companyCode;
|
||||||
|
|
||||||
return <span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{companyName}</span>;
|
return <span className={row.original.isActive === "N" ? "text-muted-foreground/70" : ""}>{companyName}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "menuName",
|
accessorKey: "menuName",
|
||||||
header: "메뉴명",
|
header: "메뉴명",
|
||||||
cell: ({ row }: any) => (
|
cell: ({ row }: any) => (
|
||||||
<span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{row.original.menuName}</span>
|
<span className={row.original.isActive === "N" ? "text-muted-foreground/70" : ""}>{row.original.menuName}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -543,8 +543,8 @@ export default function I18nPage() {
|
||||||
header: "언어 키",
|
header: "언어 키",
|
||||||
cell: ({ row }: any) => (
|
cell: ({ row }: any) => (
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer rounded p-1 hover:bg-gray-100 ${
|
className={`cursor-pointer rounded p-1 hover:bg-muted ${
|
||||||
row.original.isActive === "N" ? "text-gray-400" : ""
|
row.original.isActive === "N" ? "text-muted-foreground/70" : ""
|
||||||
}`}
|
}`}
|
||||||
onDoubleClick={() => handleEditKey(row.original)}
|
onDoubleClick={() => handleEditKey(row.original)}
|
||||||
>
|
>
|
||||||
|
|
@ -556,7 +556,7 @@ export default function I18nPage() {
|
||||||
accessorKey: "description",
|
accessorKey: "description",
|
||||||
header: "설명",
|
header: "설명",
|
||||||
cell: ({ row }: any) => (
|
cell: ({ row }: any) => (
|
||||||
<span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{row.original.description}</span>
|
<span className={row.original.isActive === "N" ? "text-muted-foreground/70" : ""}>{row.original.description}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -567,8 +567,8 @@ export default function I18nPage() {
|
||||||
onClick={() => handleToggleStatus(row.original.keyId)}
|
onClick={() => handleToggleStatus(row.original.keyId)}
|
||||||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||||
row.original.isActive === "Y"
|
row.original.isActive === "Y"
|
||||||
? "bg-green-100 text-green-800 hover:bg-green-200"
|
? "bg-emerald-100 text-emerald-800 hover:bg-emerald-200"
|
||||||
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
|
: "bg-muted text-foreground hover:bg-muted/80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{row.original.isActive === "Y" ? "활성" : "비활성"}
|
{row.original.isActive === "Y" ? "활성" : "비활성"}
|
||||||
|
|
@ -605,8 +605,8 @@ export default function I18nPage() {
|
||||||
header: "언어 코드",
|
header: "언어 코드",
|
||||||
cell: ({ row }: any) => (
|
cell: ({ row }: any) => (
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer rounded p-1 hover:bg-gray-100 ${
|
className={`cursor-pointer rounded p-1 hover:bg-muted ${
|
||||||
row.original.isActive === "N" ? "text-gray-400" : ""
|
row.original.isActive === "N" ? "text-muted-foreground/70" : ""
|
||||||
}`}
|
}`}
|
||||||
onDoubleClick={() => handleEditLanguage(row.original)}
|
onDoubleClick={() => handleEditLanguage(row.original)}
|
||||||
>
|
>
|
||||||
|
|
@ -618,14 +618,14 @@ export default function I18nPage() {
|
||||||
accessorKey: "langName",
|
accessorKey: "langName",
|
||||||
header: "언어명 (영문)",
|
header: "언어명 (영문)",
|
||||||
cell: ({ row }: any) => (
|
cell: ({ row }: any) => (
|
||||||
<span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{row.original.langName}</span>
|
<span className={row.original.isActive === "N" ? "text-muted-foreground/70" : ""}>{row.original.langName}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "langNative",
|
accessorKey: "langNative",
|
||||||
header: "언어명 (원어)",
|
header: "언어명 (원어)",
|
||||||
cell: ({ row }: any) => (
|
cell: ({ row }: any) => (
|
||||||
<span className={row.original.isActive === "N" ? "text-gray-400" : ""}>{row.original.langNative}</span>
|
<span className={row.original.isActive === "N" ? "text-muted-foreground/70" : ""}>{row.original.langNative}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -636,8 +636,8 @@ export default function I18nPage() {
|
||||||
onClick={() => handleToggleLanguageStatus(row.original.langCode)}
|
onClick={() => handleToggleLanguageStatus(row.original.langCode)}
|
||||||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||||
row.original.isActive === "Y"
|
row.original.isActive === "Y"
|
||||||
? "bg-green-100 text-green-800 hover:bg-green-200"
|
? "bg-emerald-100 text-emerald-800 hover:bg-emerald-200"
|
||||||
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
|
: "bg-muted text-foreground hover:bg-muted/80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{row.original.isActive === "Y" ? "활성" : "비활성"}
|
{row.original.isActive === "Y" ? "활성" : "비활성"}
|
||||||
|
|
@ -651,7 +651,7 @@ export default function I18nPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none px-4 py-8">
|
<div className="w-full max-w-none px-4 py-8">
|
||||||
<div className="container mx-auto p-2">
|
<div className="container mx-auto p-2">
|
||||||
{/* 탭 네비게이션 */}
|
{/* 탭 네비게이션 */}
|
||||||
|
|
@ -659,7 +659,7 @@ export default function I18nPage() {
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("keys")}
|
onClick={() => setActiveTab("keys")}
|
||||||
className={`rounded-t-lg px-3 py-1.5 text-sm font-medium transition-colors ${
|
className={`rounded-t-lg px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||||
activeTab === "keys" ? "bg-accent0 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
activeTab === "keys" ? "bg-accent0 text-white" : "bg-muted text-foreground hover:bg-muted/80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
다국어 키 관리
|
다국어 키 관리
|
||||||
|
|
@ -667,7 +667,7 @@ export default function I18nPage() {
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("languages")}
|
onClick={() => setActiveTab("languages")}
|
||||||
className={`rounded-t-lg px-3 py-1.5 text-sm font-medium transition-colors ${
|
className={`rounded-t-lg px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||||
activeTab === "languages" ? "bg-accent0 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
activeTab === "languages" ? "bg-accent0 text-white" : "bg-muted text-foreground hover:bg-muted/80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
언어 관리
|
언어 관리
|
||||||
|
|
@ -854,7 +854,7 @@ export default function I18nPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-64 items-center justify-center text-gray-500">
|
<div className="flex h-64 items-center justify-center text-muted-foreground">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-2 text-lg font-medium">언어 키를 선택하세요</div>
|
<div className="mb-2 text-lg font-medium">언어 키를 선택하세요</div>
|
||||||
<div className="text-sm">좌측 목록에서 편집할 언어 키를 클릭하세요</div>
|
<div className="text-sm">좌측 목록에서 편집할 언어 키를 클릭하세요</div>
|
||||||
|
|
|
||||||
|
|
@ -120,12 +120,12 @@ export default function TemplatesManagePage() {
|
||||||
|
|
||||||
// 간단한 아이콘 매핑 (실제로는 더 복잡한 시스템 필요)
|
// 간단한 아이콘 매핑 (실제로는 더 복잡한 시스템 필요)
|
||||||
const iconMap: Record<string, JSX.Element> = {
|
const iconMap: Record<string, JSX.Element> = {
|
||||||
table: <div className="h-4 w-4 border border-gray-400" />,
|
table: <div className="h-4 w-4 border border-input" />,
|
||||||
"mouse-pointer": <div className="h-4 w-4 rounded bg-blue-500" />,
|
"mouse-pointer": <div className="h-4 w-4 rounded bg-primary" />,
|
||||||
upload: <div className="h-4 w-4 border-2 border-dashed border-gray-400" />,
|
upload: <div className="h-4 w-4 border-2 border-dashed border-input" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
return iconMap[iconName] || <div className="h-4 w-4 rounded bg-gray-300" />;
|
return iconMap[iconName] || <div className="h-4 w-4 rounded bg-muted/60" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -133,7 +133,7 @@ export default function TemplatesManagePage() {
|
||||||
<div className="w-full max-w-none px-4 py-8">
|
<div className="w-full max-w-none px-4 py-8">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex flex-col items-center justify-center py-8">
|
<CardContent className="flex flex-col items-center justify-center py-8">
|
||||||
<p className="mb-4 text-red-600">템플릿 목록을 불러오는 중 오류가 발생했습니다.</p>
|
<p className="mb-4 text-destructive">템플릿 목록을 불러오는 중 오류가 발생했습니다.</p>
|
||||||
<Button onClick={() => refetch()} variant="outline">
|
<Button onClick={() => refetch()} variant="outline">
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
다시 시도
|
다시 시도
|
||||||
|
|
@ -145,13 +145,13 @@ export default function TemplatesManagePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-muted">
|
||||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||||
{/* 페이지 제목 */}
|
{/* 페이지 제목 */}
|
||||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">템플릿 관리</h1>
|
<h1 className="text-3xl font-bold text-foreground">템플릿 관리</h1>
|
||||||
<p className="mt-2 text-gray-600">화면 디자이너에서 사용할 템플릿을 관리합니다</p>
|
<p className="mt-2 text-muted-foreground">화면 디자이너에서 사용할 템플릿을 관리합니다</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button asChild className="shadow-sm">
|
<Button asChild className="shadow-sm">
|
||||||
|
|
@ -164,9 +164,9 @@ export default function TemplatesManagePage() {
|
||||||
|
|
||||||
{/* 필터 및 검색 */}
|
{/* 필터 및 검색 */}
|
||||||
<Card className="shadow-sm">
|
<Card className="shadow-sm">
|
||||||
<CardHeader className="bg-gray-50/50">
|
<CardHeader className="bg-muted/50">
|
||||||
<CardTitle className="flex items-center">
|
<CardTitle className="flex items-center">
|
||||||
<Filter className="mr-2 h-5 w-5 text-gray-600" />
|
<Filter className="mr-2 h-5 w-5 text-muted-foreground" />
|
||||||
필터 및 검색
|
필터 및 검색
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -176,7 +176,7 @@ export default function TemplatesManagePage() {
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">검색</label>
|
<label className="text-sm font-medium">검색</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="템플릿명, 설명 검색..."
|
placeholder="템플릿명, 설명 검색..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
|
|
@ -232,7 +232,7 @@ export default function TemplatesManagePage() {
|
||||||
|
|
||||||
{/* 템플릿 목록 테이블 */}
|
{/* 템플릿 목록 테이블 */}
|
||||||
<Card className="shadow-sm">
|
<Card className="shadow-sm">
|
||||||
<CardHeader className="bg-gray-50/50">
|
<CardHeader className="bg-muted/50">
|
||||||
<CardTitle>템플릿 목록 ({filteredAndSortedTemplates.length}개)</CardTitle>
|
<CardTitle>템플릿 목록 ({filteredAndSortedTemplates.length}개)</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
@ -293,7 +293,7 @@ export default function TemplatesManagePage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : filteredAndSortedTemplates.length === 0 ? (
|
) : filteredAndSortedTemplates.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={11} className="py-8 text-center text-gray-500">
|
<TableCell colSpan={11} className="py-8 text-center text-muted-foreground">
|
||||||
검색 조건에 맞는 템플릿이 없습니다.
|
검색 조건에 맞는 템플릿이 없습니다.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -357,7 +357,7 @@ export default function TemplatesManagePage() {
|
||||||
</Button>
|
</Button>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button size="sm" variant="ghost" className="text-red-600 hover:text-red-700">
|
<Button size="sm" variant="ghost" className="text-destructive hover:text-destructive">
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
|
|
@ -373,7 +373,7 @@ export default function TemplatesManagePage() {
|
||||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={() => handleDelete(template.template_code, template.template_name)}
|
onClick={() => handleDelete(template.template_code, template.template_name)}
|
||||||
className="bg-red-600 hover:bg-red-700"
|
className="bg-destructive hover:bg-red-700"
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
>
|
>
|
||||||
삭제
|
삭제
|
||||||
|
|
|
||||||
|
|
@ -25,27 +25,27 @@ export default function TestPage() {
|
||||||
<h1 className="mb-4 text-2xl font-bold">토큰 테스트 페이지</h1>
|
<h1 className="mb-4 text-2xl font-bold">토큰 테스트 페이지</h1>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded bg-green-100 p-4">
|
<div className="rounded bg-emerald-100 p-4">
|
||||||
<h2 className="mb-2 font-semibold">토큰 상태</h2>
|
<h2 className="mb-2 font-semibold">토큰 상태</h2>
|
||||||
<p>토큰 존재: {tokenInfo.hasToken ? "✅ 예" : "❌ 아니오"}</p>
|
<p>토큰 존재: {tokenInfo.hasToken ? "✅ 예" : "❌ 아니오"}</p>
|
||||||
<p>토큰 길이: {tokenInfo.tokenLength}</p>
|
<p>토큰 길이: {tokenInfo.tokenLength}</p>
|
||||||
<p>토큰 시작: {tokenInfo.tokenStart}</p>
|
<p>토큰 시작: {tokenInfo.tokenStart}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded bg-blue-100 p-4">
|
<div className="rounded bg-primary/10 p-4">
|
||||||
<h2 className="mb-2 font-semibold">페이지 정보</h2>
|
<h2 className="mb-2 font-semibold">페이지 정보</h2>
|
||||||
<p>현재 URL: {tokenInfo.currentUrl}</p>
|
<p>현재 URL: {tokenInfo.currentUrl}</p>
|
||||||
<p>시간: {tokenInfo.timestamp}</p>
|
<p>시간: {tokenInfo.timestamp}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded bg-yellow-100 p-4">
|
<div className="rounded bg-amber-100 p-4">
|
||||||
<h2 className="mb-2 font-semibold">테스트 버튼</h2>
|
<h2 className="mb-2 font-semibold">테스트 버튼</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const token = localStorage.getItem("authToken");
|
const token = localStorage.getItem("authToken");
|
||||||
alert(`토큰: ${token ? "존재" : "없음"}\n길이: ${token ? token.length : 0}`);
|
alert(`토큰: ${token ? "존재" : "없음"}\n길이: ${token ? token.length : 0}`);
|
||||||
}}
|
}}
|
||||||
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
className="rounded bg-primary px-4 py-2 text-white hover:bg-primary"
|
||||||
>
|
>
|
||||||
토큰 확인
|
토큰 확인
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,12 @@ export default function TokenTestPage() {
|
||||||
<h1 className="mb-4 text-2xl font-bold">토큰 상태 테스트</h1>
|
<h1 className="mb-4 text-2xl font-bold">토큰 상태 테스트</h1>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded bg-gray-100 p-4">
|
<div className="rounded bg-muted p-4">
|
||||||
<h2 className="mb-2 font-semibold">토큰 정보</h2>
|
<h2 className="mb-2 font-semibold">토큰 정보</h2>
|
||||||
<pre className="text-sm">{JSON.stringify(tokenInfo, null, 2)}</pre>
|
<pre className="text-sm">{JSON.stringify(tokenInfo, null, 2)}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded bg-blue-100 p-4">
|
<div className="rounded bg-primary/10 p-4">
|
||||||
<h2 className="mb-2 font-semibold">테스트 버튼</h2>
|
<h2 className="mb-2 font-semibold">테스트 버튼</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -44,7 +44,7 @@ export default function TokenTestPage() {
|
||||||
console.log("현재 토큰:", token);
|
console.log("현재 토큰:", token);
|
||||||
alert(`토큰: ${token ? "존재" : "없음"}`);
|
alert(`토큰: ${token ? "존재" : "없음"}`);
|
||||||
}}
|
}}
|
||||||
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
className="rounded bg-primary px-4 py-2 text-white hover:bg-primary"
|
||||||
>
|
>
|
||||||
토큰 확인
|
토큰 확인
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`rounded-full px-3 py-1 text-sm font-medium ${
|
className={`rounded-full px-3 py-1 text-sm font-medium ${
|
||||||
roleGroup.status === "active" ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
|
roleGroup.status === "active" ? "bg-emerald-100 text-emerald-800" : "bg-muted text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{roleGroup.status === "active" ? "활성" : "비활성"}
|
{roleGroup.status === "active" ? "활성" : "비활성"}
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ export default function RolesPage() {
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`rounded-full px-2 py-1 text-xs font-medium ${
|
className={`rounded-full px-2 py-1 text-xs font-medium ${
|
||||||
role.status === "active" ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
|
role.status === "active" ? "bg-emerald-100 text-emerald-800" : "bg-muted text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{role.status === "active" ? "활성" : "비활성"}
|
{role.status === "active" ? "활성" : "비활성"}
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,7 @@ export default function ValidationDemoPage() {
|
||||||
<CardDescription>실시간 검증이 적용된 폼입니다. 입력하면서 검증 결과를 확인해보세요.</CardDescription>
|
<CardDescription>실시간 검증이 적용된 폼입니다. 입력하면서 검증 결과를 확인해보세요.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="relative min-h-[400px] rounded-lg border border-dashed border-gray-300 p-4">
|
<div className="relative min-h-[400px] rounded-lg border border-dashed border-input p-4">
|
||||||
<EnhancedInteractiveScreenViewer
|
<EnhancedInteractiveScreenViewer
|
||||||
component={TEST_COMPONENTS[0]} // container
|
component={TEST_COMPONENTS[0]} // container
|
||||||
allComponents={TEST_COMPONENTS}
|
allComponents={TEST_COMPONENTS}
|
||||||
|
|
@ -485,7 +485,7 @@ export default function ValidationDemoPage() {
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="font-semibold">폼 데이터</h4>
|
<h4 className="font-semibold">폼 데이터</h4>
|
||||||
<pre className="max-h-60 overflow-auto rounded-md bg-gray-100 p-3 text-sm">
|
<pre className="max-h-60 overflow-auto rounded-md bg-muted p-3 text-sm">
|
||||||
{JSON.stringify(formData, null, 2)}
|
{JSON.stringify(formData, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -493,15 +493,15 @@ export default function ValidationDemoPage() {
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="font-semibold">검증 통계</h4>
|
<h4 className="font-semibold">검증 통계</h4>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="rounded-md bg-green-50 p-3">
|
<div className="rounded-md bg-emerald-50 p-3">
|
||||||
<div className="text-lg font-bold text-green-600">
|
<div className="text-lg font-bold text-emerald-600">
|
||||||
{Object.values(validationState.fieldStates).filter((f) => f.status === "valid").length}
|
{Object.values(validationState.fieldStates).filter((f) => f.status === "valid").length}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-green-700">유효한 필드</div>
|
<div className="text-sm text-emerald-700">유효한 필드</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md bg-red-50 p-3">
|
<div className="rounded-md bg-destructive/10 p-3">
|
||||||
<div className="text-lg font-bold text-red-600">{validationState.errors.length}</div>
|
<div className="text-lg font-bold text-destructive">{validationState.errors.length}</div>
|
||||||
<div className="text-sm text-red-700">오류 개수</div>
|
<div className="text-sm text-destructive">오류 개수</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const statusConfig: Record<string, { label: string; variant: "default" | "second
|
||||||
const lineStatusConfig: Record<string, { label: string; icon: React.ReactNode }> = {
|
const lineStatusConfig: Record<string, { label: string; icon: React.ReactNode }> = {
|
||||||
waiting: { label: "대기", icon: <Clock className="h-3 w-3 text-muted-foreground" /> },
|
waiting: { label: "대기", icon: <Clock className="h-3 w-3 text-muted-foreground" /> },
|
||||||
pending: { label: "진행 중", icon: <Clock className="h-3 w-3 text-primary" /> },
|
pending: { label: "진행 중", icon: <Clock className="h-3 w-3 text-primary" /> },
|
||||||
approved: { label: "승인", icon: <CheckCircle2 className="h-3 w-3 text-green-600" /> },
|
approved: { label: "승인", icon: <CheckCircle2 className="h-3 w-3 text-emerald-600" /> },
|
||||||
rejected: { label: "반려", icon: <XCircle className="h-3 w-3 text-destructive" /> },
|
rejected: { label: "반려", icon: <XCircle className="h-3 w-3 text-destructive" /> },
|
||||||
skipped: { label: "건너뜀", icon: <Clock className="h-3 w-3 text-muted-foreground" /> },
|
skipped: { label: "건너뜀", icon: <Clock className="h-3 w-3 text-muted-foreground" /> },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -107,18 +107,18 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen">
|
<div className="h-screen">
|
||||||
{/* 대시보드 헤더 - 보기 모드에서는 숨김 */}
|
{/* 대시보드 헤더 - 보기 모드에서는 숨김 */}
|
||||||
{/* <div className="border-b border-gray-200 bg-white px-6 py-4">
|
{/* <div className="border-b border-border bg-white px-6 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-800">{dashboard.title}</h1>
|
<h1 className="text-2xl font-bold text-foreground">{dashboard.title}</h1>
|
||||||
{dashboard.description && <p className="mt-1 text-sm text-gray-600">{dashboard.description}</p>}
|
{dashboard.description && <p className="mt-1 text-sm text-muted-foreground">{dashboard.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* 새로고침 버튼 *\/}
|
{/* 새로고침 버튼 *\/}
|
||||||
<button
|
<button
|
||||||
onClick={loadDashboard}
|
onClick={loadDashboard}
|
||||||
className="rounded-lg border border-gray-300 px-3 py-2 text-gray-600 hover:bg-gray-50 hover:text-gray-800"
|
className="rounded-lg border border-input px-3 py-2 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
title="새로고침"
|
title="새로고침"
|
||||||
>
|
>
|
||||||
🔄
|
🔄
|
||||||
|
|
@ -133,7 +133,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
||||||
document.documentElement.requestFullscreen();
|
document.documentElement.requestFullscreen();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="rounded-lg border border-gray-300 px-3 py-2 text-gray-600 hover:bg-gray-50 hover:text-gray-800"
|
className="rounded-lg border border-input px-3 py-2 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
title="전체화면"
|
title="전체화면"
|
||||||
>
|
>
|
||||||
⛶
|
⛶
|
||||||
|
|
@ -144,7 +144,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(`/admin/screenMng/dashboardList?load=${resolvedParams.dashboardId}`);
|
router.push(`/admin/screenMng/dashboardList?load=${resolvedParams.dashboardId}`);
|
||||||
}}
|
}}
|
||||||
className="rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
className="rounded-lg bg-primary px-4 py-2 text-white hover:bg-primary"
|
||||||
>
|
>
|
||||||
편집
|
편집
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -152,7 +152,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 메타 정보 *\/}
|
{/* 메타 정보 *\/}
|
||||||
<div className="mt-2 flex items-center gap-4 text-xs text-gray-500">
|
<div className="mt-2 flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
<span>생성: {new Date(dashboard.createdAt).toLocaleString()}</span>
|
<span>생성: {new Date(dashboard.createdAt).toLocaleString()}</span>
|
||||||
<span>수정: {new Date(dashboard.updatedAt).toLocaleString()}</span>
|
<span>수정: {new Date(dashboard.updatedAt).toLocaleString()}</span>
|
||||||
<span>요소: {dashboard.elements.length}개</span>
|
<span>요소: {dashboard.elements.length}개</span>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
||||||
import { FileCheck, Menu, Users, Bell, FileText, Layout, Server, Shield, Calendar } from "lucide-react";
|
import { FileCheck, Menu, Users, Bell, FileText, Layout, Server, Shield, Calendar } from "lucide-react";
|
||||||
|
|
||||||
const quickAccessItems = [
|
const quickAccessItems = [
|
||||||
{ label: "결재함", icon: FileCheck, href: "/admin/approvalBox", color: "text-blue-600 bg-blue-50" },
|
{ label: "결재함", icon: FileCheck, href: "/admin/approvalBox", color: "text-primary bg-primary/10" },
|
||||||
{ label: "메뉴 관리", icon: Menu, href: "/admin/menu", color: "text-violet-600 bg-violet-50" },
|
{ label: "메뉴 관리", icon: Menu, href: "/admin/menu", color: "text-violet-600 bg-violet-50" },
|
||||||
{ label: "사용자 관리", icon: Users, href: "/admin/userMng", color: "text-emerald-600 bg-emerald-50" },
|
{ label: "사용자 관리", icon: Users, href: "/admin/userMng", color: "text-emerald-600 bg-emerald-50" },
|
||||||
{ label: "공지사항", icon: Bell, href: "/admin/system-notices", color: "text-amber-600 bg-amber-50" },
|
{ label: "공지사항", icon: Bell, href: "/admin/system-notices", color: "text-amber-600 bg-amber-50" },
|
||||||
|
|
|
||||||
|
|
@ -385,13 +385,13 @@ export default function MultiLangPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<div className="text-sm text-gray-600">검색 결과: {filteredLangKeys.length}건</div>
|
<div className="text-sm text-muted-foreground">검색 결과: {filteredLangKeys.length}건</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="text-sm text-gray-600">전체: {filteredLangKeys.length}건</div>
|
<div className="text-sm text-muted-foreground">전체: {filteredLangKeys.length}건</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|
@ -417,7 +417,7 @@ export default function MultiLangPage() {
|
||||||
{languages.map((lang) => (
|
{languages.map((lang) => (
|
||||||
<div key={lang.langCode} className="rounded-lg border p-4">
|
<div key={lang.langCode} className="rounded-lg border p-4">
|
||||||
<div className="font-semibold">{lang.langName}</div>
|
<div className="font-semibold">{lang.langName}</div>
|
||||||
<div className="text-sm text-gray-600">{lang.langNative}</div>
|
<div className="text-sm text-muted-foreground">{lang.langNative}</div>
|
||||||
<Badge variant={lang.isActive === "Y" ? "default" : "secondary"} className="mt-2">
|
<Badge variant={lang.isActive === "Y" ? "default" : "secondary"} className="mt-2">
|
||||||
{lang.isActive === "Y" ? "활성" : "비활성"}
|
{lang.isActive === "Y" ? "활성" : "비활성"}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -440,7 +440,7 @@ export default function MultiLangPage() {
|
||||||
<h3 className="mb-2 font-semibold">{company.name}</h3>
|
<h3 className="mb-2 font-semibold">{company.name}</h3>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{menus.map((menu) => (
|
{menus.map((menu) => (
|
||||||
<div key={menu.code} className="text-sm text-gray-600">
|
<div key={menu.code} className="text-sm text-muted-foreground">
|
||||||
{menu.name}
|
{menu.name}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
||||||
import { FileCheck, Menu, Users, Bell, FileText, Layout, Server, Shield, Calendar, ArrowRight } from "lucide-react";
|
import { FileCheck, Menu, Users, Bell, FileText, Layout, Server, Shield, Calendar, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
const quickAccessItems = [
|
const quickAccessItems = [
|
||||||
{ label: "결재함", icon: FileCheck, href: "/admin/approvalBox", color: "text-blue-600 bg-blue-50" },
|
{ label: "결재함", icon: FileCheck, href: "/admin/approvalBox", color: "text-primary bg-primary/10" },
|
||||||
{ label: "메뉴 관리", icon: Menu, href: "/admin/menu", color: "text-violet-600 bg-violet-50" },
|
{ label: "메뉴 관리", icon: Menu, href: "/admin/menu", color: "text-violet-600 bg-violet-50" },
|
||||||
{ label: "사용자 관리", icon: Users, href: "/admin/userMng", color: "text-emerald-600 bg-emerald-50" },
|
{ label: "사용자 관리", icon: Users, href: "/admin/userMng", color: "text-emerald-600 bg-emerald-50" },
|
||||||
{ label: "공지사항", icon: Bell, href: "/admin/system-notices", color: "text-amber-600 bg-amber-50" },
|
{ label: "공지사항", icon: Bell, href: "/admin/system-notices", color: "text-amber-600 bg-amber-50" },
|
||||||
|
|
|
||||||
|
|
@ -174,10 +174,10 @@ function PopScreenViewPage() {
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full items-center justify-center bg-gray-100">
|
<div className="flex h-screen w-full items-center justify-center bg-muted">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="mx-auto h-10 w-10 animate-spin text-blue-500" />
|
<Loader2 className="mx-auto h-10 w-10 animate-spin text-primary" />
|
||||||
<p className="mt-4 text-gray-600">POP 화면 로딩 중...</p>
|
<p className="mt-4 text-muted-foreground">POP 화면 로딩 중...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -185,13 +185,13 @@ function PopScreenViewPage() {
|
||||||
|
|
||||||
if (error || !screen) {
|
if (error || !screen) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full items-center justify-center bg-gray-100">
|
<div className="flex h-screen w-full items-center justify-center bg-muted">
|
||||||
<div className="text-center max-w-md p-6">
|
<div className="text-center max-w-md p-6">
|
||||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10">
|
||||||
<span className="text-2xl">!</span>
|
<span className="text-2xl">!</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mb-2 text-xl font-bold text-gray-800">화면을 찾을 수 없습니다</h2>
|
<h2 className="mb-2 text-xl font-bold text-foreground">화면을 찾을 수 없습니다</h2>
|
||||||
<p className="mb-4 text-gray-600">{error || "요청하신 POP 화면이 존재하지 않습니다."}</p>
|
<p className="mb-4 text-muted-foreground">{error || "요청하신 POP 화면이 존재하지 않습니다."}</p>
|
||||||
<Button onClick={() => router.back()} variant="outline">
|
<Button onClick={() => router.back()} variant="outline">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
돌아가기
|
돌아가기
|
||||||
|
|
@ -205,7 +205,7 @@ function PopScreenViewPage() {
|
||||||
<ScreenPreviewProvider isPreviewMode={isPreviewMode}>
|
<ScreenPreviewProvider isPreviewMode={isPreviewMode}>
|
||||||
<ActiveTabProvider>
|
<ActiveTabProvider>
|
||||||
<TableOptionsProvider>
|
<TableOptionsProvider>
|
||||||
<div className="h-screen bg-gray-100 flex flex-col">
|
<div className="h-screen bg-muted flex flex-col">
|
||||||
{/* 상단 툴바 (프리뷰 모드에서만) */}
|
{/* 상단 툴바 (프리뷰 모드에서만) */}
|
||||||
{isPreviewMode && (
|
{isPreviewMode && (
|
||||||
<div className="sticky top-0 z-50 bg-white border-b shadow-sm">
|
<div className="sticky top-0 z-50 bg-white border-b shadow-sm">
|
||||||
|
|
@ -216,13 +216,13 @@ function PopScreenViewPage() {
|
||||||
닫기
|
닫기
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm font-medium">{screen.screenName}</span>
|
<span className="text-sm font-medium">{screen.screenName}</span>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-muted-foreground/70">
|
||||||
({currentModeKey.replace("_", " ")})
|
({currentModeKey.replace("_", " ")})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-1">
|
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
||||||
<Button
|
<Button
|
||||||
variant={deviceType === "mobile" ? "default" : "ghost"}
|
variant={deviceType === "mobile" ? "default" : "ghost"}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -243,7 +243,7 @@ function PopScreenViewPage() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-1">
|
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
||||||
<Button
|
<Button
|
||||||
variant={isLandscape ? "default" : "ghost"}
|
variant={isLandscape ? "default" : "ghost"}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -295,7 +295,7 @@ function PopScreenViewPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-gray-800" : "w-full min-h-full"}`}
|
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-foreground" : "w-full min-h-full"}`}
|
||||||
style={isPreviewMode ? {
|
style={isPreviewMode ? {
|
||||||
width: currentDevice.width,
|
width: currentDevice.width,
|
||||||
maxHeight: "80vh",
|
maxHeight: "80vh",
|
||||||
|
|
@ -333,13 +333,13 @@ function PopScreenViewPage() {
|
||||||
) : (
|
) : (
|
||||||
// 빈 화면
|
// 빈 화면
|
||||||
<div className="flex flex-col items-center justify-center min-h-[400px] p-8 text-center">
|
<div className="flex flex-col items-center justify-center min-h-[400px] p-8 text-center">
|
||||||
<div className="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
||||||
<Smartphone className="h-8 w-8 text-gray-400" />
|
<Smartphone className="h-8 w-8 text-muted-foreground/70" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">
|
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||||
화면이 비어있습니다
|
화면이 비어있습니다
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500 max-w-xs">
|
<p className="text-sm text-muted-foreground max-w-xs">
|
||||||
POP 화면 디자이너에서 컴포넌트를 추가하여 화면을 구성하세요.
|
POP 화면 디자이너에서 컴포넌트를 추가하여 화면을 구성하세요.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -227,9 +227,9 @@ export default function SimpleTypeSafetyTest() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{passedTests === totalTests && totalTests > 0 && (
|
{passedTests === totalTests && totalTests > 0 && (
|
||||||
<div className="mt-4 rounded-lg border border-green-200 bg-green-50 p-4">
|
<div className="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 p-4">
|
||||||
<div className="font-medium text-green-800">🎉 모든 타입 안전성 테스트가 통과되었습니다!</div>
|
<div className="font-medium text-emerald-800">🎉 모든 타입 안전성 테스트가 통과되었습니다!</div>
|
||||||
<div className="mt-2 text-sm text-green-600">
|
<div className="mt-2 text-sm text-emerald-600">
|
||||||
화면관리, 제어관리, 테이블타입관리 시스템이 안전하게 작동합니다.
|
화면관리, 제어관리, 테이블타입관리 시스템이 안전하게 작동합니다.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export default function StressTestPage() {
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<h1 className="text-3xl font-bold">🔥 타입 시스템 스트레스 테스트</h1>
|
<h1 className="text-3xl font-bold">🔥 타입 시스템 스트레스 테스트</h1>
|
||||||
<p className="text-muted-foreground">극한 상황에서 타입 시스템의 견고함과 성능을 검증합니다</p>
|
<p className="text-muted-foreground">극한 상황에서 타입 시스템의 견고함과 성능을 검증합니다</p>
|
||||||
<div className="rounded-lg bg-orange-50 p-3 text-sm text-orange-600">
|
<div className="rounded-lg bg-amber-50 p-3 text-sm text-amber-600">
|
||||||
⚠️ 주의: 이 테스트는 시스템에 높은 부하를 가할 수 있습니다
|
⚠️ 주의: 이 테스트는 시스템에 높은 부하를 가할 수 있습니다
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -184,19 +184,19 @@ export default function StressTestPage() {
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-green-600">{testResults.passedTests}</div>
|
<div className="text-2xl font-bold text-emerald-600">{testResults.passedTests}</div>
|
||||||
<div className="text-muted-foreground text-sm">통과</div>
|
<div className="text-muted-foreground text-sm">통과</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-red-600">{testResults.failedTests}</div>
|
<div className="text-2xl font-bold text-destructive">{testResults.failedTests}</div>
|
||||||
<div className="text-muted-foreground text-sm">실패</div>
|
<div className="text-muted-foreground text-sm">실패</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-yellow-600">{testResults.warningTests}</div>
|
<div className="text-2xl font-bold text-amber-600">{testResults.warningTests}</div>
|
||||||
<div className="text-muted-foreground text-sm">경고</div>
|
<div className="text-muted-foreground text-sm">경고</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-blue-600">{Math.round(testResults.totalDuration)}ms</div>
|
<div className="text-2xl font-bold text-primary">{Math.round(testResults.totalDuration)}ms</div>
|
||||||
<div className="text-muted-foreground text-sm">총 소요시간</div>
|
<div className="text-muted-foreground text-sm">총 소요시간</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -212,16 +212,16 @@ export default function StressTestPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{testResults.success ? (
|
{testResults.success ? (
|
||||||
<div className="rounded-lg border border-green-200 bg-green-50 p-4">
|
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-4">
|
||||||
<div className="font-medium text-green-800">🎉 모든 스트레스 테스트 통과!</div>
|
<div className="font-medium text-emerald-800">🎉 모든 스트레스 테스트 통과!</div>
|
||||||
<div className="mt-2 text-sm text-green-600">
|
<div className="mt-2 text-sm text-emerald-600">
|
||||||
타입 시스템이 극한 상황에서도 안정적으로 작동합니다.
|
타입 시스템이 극한 상황에서도 안정적으로 작동합니다.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
|
<div className="rounded-lg border border-destructive/20 bg-destructive/10 p-4">
|
||||||
<div className="font-medium text-red-800">⚠️ 일부 스트레스 테스트 실패</div>
|
<div className="font-medium text-red-800">⚠️ 일부 스트레스 테스트 실패</div>
|
||||||
<div className="mt-2 text-sm text-red-600">
|
<div className="mt-2 text-sm text-destructive">
|
||||||
개선이 필요한 영역이 있습니다. 아래 권장사항을 확인하세요.
|
개선이 필요한 영역이 있습니다. 아래 권장사항을 확인하세요.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -253,7 +253,7 @@ export default function StressTestPage() {
|
||||||
|
|
||||||
{/* 메트릭스 표시 */}
|
{/* 메트릭스 표시 */}
|
||||||
{result.metrics && (
|
{result.metrics && (
|
||||||
<div className="mt-3 rounded bg-gray-50 p-3 text-xs">
|
<div className="mt-3 rounded bg-muted p-3 text-xs">
|
||||||
<div className="mb-1 font-medium">📊 상세 메트릭스:</div>
|
<div className="mb-1 font-medium">📊 상세 메트릭스:</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{Object.entries(result.metrics).map(([key, value]) => (
|
{Object.entries(result.metrics).map(([key, value]) => (
|
||||||
|
|
@ -286,7 +286,7 @@ export default function StressTestPage() {
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{testResults.recommendation.map((rec, index) => (
|
{testResults.recommendation.map((rec, index) => (
|
||||||
<li key={index} className="flex items-start gap-2">
|
<li key={index} className="flex items-start gap-2">
|
||||||
<span className="mt-0.5 text-blue-500">•</span>
|
<span className="mt-0.5 text-primary">•</span>
|
||||||
<span className="text-sm">{rec}</span>
|
<span className="text-sm">{rec}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -304,7 +304,7 @@ export default function StressTestPage() {
|
||||||
<CardTitle className="text-lg">📋 테스트 로그 (최근 10개)</CardTitle>
|
<CardTitle className="text-lg">📋 테스트 로그 (최근 10개)</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="h-40 overflow-y-auto rounded bg-black p-4 font-mono text-xs text-green-400">
|
<div className="h-40 overflow-y-auto rounded bg-black p-4 font-mono text-xs text-emerald-400">
|
||||||
{testLogs.slice(-10).map((log, index) => (
|
{testLogs.slice(-10).map((log, index) => (
|
||||||
<div key={index}>{log}</div>
|
<div key={index}>{log}</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,10 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
||||||
return <Video {...iconProps} className="text-purple-600" />;
|
return <Video {...iconProps} className="text-purple-600" />;
|
||||||
}
|
}
|
||||||
if (['mp3', 'wav', 'flac', 'aac', 'ogg'].includes(extension)) {
|
if (['mp3', 'wav', 'flac', 'aac', 'ogg'].includes(extension)) {
|
||||||
return <Music {...iconProps} className="text-green-600" />;
|
return <Music {...iconProps} className="text-emerald-600" />;
|
||||||
}
|
}
|
||||||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) {
|
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) {
|
||||||
return <Archive {...iconProps} className="text-yellow-600" />;
|
return <Archive {...iconProps} className="text-amber-600" />;
|
||||||
}
|
}
|
||||||
if (['txt', 'md', 'doc', 'docx', 'pdf', 'rtf'].includes(extension)) {
|
if (['txt', 'md', 'doc', 'docx', 'pdf', 'rtf'].includes(extension)) {
|
||||||
return <FileText {...iconProps} className="text-destructive" />;
|
return <FileText {...iconProps} className="text-destructive" />;
|
||||||
|
|
@ -192,7 +192,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
||||||
{showControls && (
|
{showControls && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground/70" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="파일명으로 검색..."
|
placeholder="파일명으로 검색..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
|
|
@ -219,7 +219,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
||||||
style={{ maxHeight }}
|
style={{ maxHeight }}
|
||||||
>
|
>
|
||||||
{filteredFiles.length === 0 ? (
|
{filteredFiles.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
{searchQuery ? "검색 결과가 없습니다." : "저장된 파일이 없습니다."}
|
{searchQuery ? "검색 결과가 없습니다." : "저장된 파일이 없습니다."}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -232,7 +232,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
||||||
<div className="font-medium truncate">
|
<div className="font-medium truncate">
|
||||||
{file.realFileName || file.savedFileName}
|
{file.realFileName || file.savedFileName}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500 flex items-center gap-2">
|
<div className="text-sm text-muted-foreground flex items-center gap-2">
|
||||||
<span>{formatFileSize(file.fileSize)}</span>
|
<span>{formatFileSize(file.fileSize)}</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
|
|
@ -273,7 +273,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleRemove(file)}
|
onClick={() => handleRemove(file)}
|
||||||
className="flex items-center gap-1 text-destructive hover:text-red-700"
|
className="flex items-center gap-1 text-destructive hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="columnName">
|
<Label htmlFor="columnName">
|
||||||
컬럼명 <span className="text-red-500">*</span>
|
컬럼명 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="columnName"
|
id="columnName"
|
||||||
|
|
@ -233,7 +233,7 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
onChange={(e) => updateColumn({ name: e.target.value })}
|
onChange={(e) => updateColumn({ name: e.target.value })}
|
||||||
placeholder="column_name"
|
placeholder="column_name"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={validationErrors.some((e) => e.includes("컬럼명")) ? "border-red-300" : ""}
|
className={validationErrors.some((e) => e.includes("컬럼명")) ? "border-destructive/30" : ""}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground text-xs">영문자로 시작, 영문자/숫자/언더스코어만 사용 가능</p>
|
<p className="text-muted-foreground text-xs">영문자로 시작, 영문자/숫자/언더스코어만 사용 가능</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -255,7 +255,7 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>
|
<Label>
|
||||||
입력타입 <span className="text-red-500">*</span>
|
입력타입 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={column.inputType} onValueChange={handleInputTypeChange} disabled={loading}>
|
<Select value={column.inputType} onValueChange={handleInputTypeChange} disabled={loading}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -354,7 +354,7 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAddColumn}
|
onClick={handleAddColumn}
|
||||||
disabled={!isFormValid || loading}
|
disabled={!isFormValid || loading}
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-primary hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export function AuthenticationConfig({
|
||||||
|
|
||||||
{/* 인증 타입별 설정 필드 */}
|
{/* 인증 타입별 설정 필드 */}
|
||||||
{authType === "api-key" && (
|
{authType === "api-key" && (
|
||||||
<div className="space-y-4 rounded-md border bg-gray-50 p-4">
|
<div className="space-y-4 rounded-md border bg-muted p-4">
|
||||||
<h4 className="text-sm font-medium">API Key 설정</h4>
|
<h4 className="text-sm font-medium">API Key 설정</h4>
|
||||||
|
|
||||||
{/* 키 위치 */}
|
{/* 키 위치 */}
|
||||||
|
|
@ -96,7 +96,7 @@ export function AuthenticationConfig({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{authType === "bearer" && (
|
{authType === "bearer" && (
|
||||||
<div className="space-y-4 rounded-md border bg-gray-50 p-4">
|
<div className="space-y-4 rounded-md border bg-muted p-4">
|
||||||
<h4 className="text-sm font-medium">Bearer Token 설정</h4>
|
<h4 className="text-sm font-medium">Bearer Token 설정</h4>
|
||||||
|
|
||||||
{/* 토큰 */}
|
{/* 토큰 */}
|
||||||
|
|
@ -111,14 +111,14 @@ export function AuthenticationConfig({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
* Authorization 헤더에 "Bearer {token}" 형식으로 전송됩니다.
|
* Authorization 헤더에 "Bearer {token}" 형식으로 전송됩니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{authType === "basic" && (
|
{authType === "basic" && (
|
||||||
<div className="space-y-4 rounded-md border bg-gray-50 p-4">
|
<div className="space-y-4 rounded-md border bg-muted p-4">
|
||||||
<h4 className="text-sm font-medium">Basic Auth 설정</h4>
|
<h4 className="text-sm font-medium">Basic Auth 설정</h4>
|
||||||
|
|
||||||
{/* 사용자명 */}
|
{/* 사용자명 */}
|
||||||
|
|
@ -145,12 +145,12 @@ export function AuthenticationConfig({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500">* Authorization 헤더에 Base64 인코딩된 인증 정보가 전송됩니다.</p>
|
<p className="text-xs text-muted-foreground">* Authorization 헤더에 Base64 인코딩된 인증 정보가 전송됩니다.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{authType === "oauth2" && (
|
{authType === "oauth2" && (
|
||||||
<div className="space-y-4 rounded-md border bg-gray-50 p-4">
|
<div className="space-y-4 rounded-md border bg-muted p-4">
|
||||||
<h4 className="text-sm font-medium">OAuth 2.0 설정</h4>
|
<h4 className="text-sm font-medium">OAuth 2.0 설정</h4>
|
||||||
|
|
||||||
{/* Client ID */}
|
{/* Client ID */}
|
||||||
|
|
@ -189,12 +189,12 @@ export function AuthenticationConfig({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500">* OAuth 2.0 Client Credentials Grant 방식을 사용합니다.</p>
|
<p className="text-xs text-muted-foreground">* OAuth 2.0 Client Credentials Grant 방식을 사용합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{authType === "db-token" && (
|
{authType === "db-token" && (
|
||||||
<div className="space-y-4 rounded-md border bg-gray-50 p-4">
|
<div className="space-y-4 rounded-md border bg-muted p-4">
|
||||||
<h4 className="text-sm font-medium">DB 기반 토큰 설정</h4>
|
<h4 className="text-sm font-medium">DB 기반 토큰 설정</h4>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -275,14 +275,14 @@ export function AuthenticationConfig({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
company_code는 현재 로그인한 사용자의 회사 코드로 자동 필터링됩니다.
|
company_code는 현재 로그인한 사용자의 회사 코드로 자동 필터링됩니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{authType === "none" && (
|
{authType === "none" && (
|
||||||
<div className="rounded-md border border-dashed p-4 text-center text-sm text-gray-500">
|
<div className="rounded-md border border-dashed p-4 text-center text-sm text-muted-foreground">
|
||||||
인증이 필요하지 않은 공개 API입니다.
|
인증이 필요하지 않은 공개 API입니다.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -341,14 +341,14 @@ export function CreateTableModal({
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="tableName">
|
<Label htmlFor="tableName">
|
||||||
테이블명 <span className="text-red-500">*</span>
|
테이블명 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="tableName"
|
id="tableName"
|
||||||
value={tableName}
|
value={tableName}
|
||||||
onChange={(e) => handleTableNameChange(e.target.value)}
|
onChange={(e) => handleTableNameChange(e.target.value)}
|
||||||
placeholder="예: customer_info"
|
placeholder="예: customer_info"
|
||||||
className={tableNameError ? "border-red-300" : ""}
|
className={tableNameError ? "border-destructive/30" : ""}
|
||||||
/>
|
/>
|
||||||
{tableNameError && <p className="text-destructive text-sm">{tableNameError}</p>}
|
{tableNameError && <p className="text-destructive text-sm">{tableNameError}</p>}
|
||||||
<p className="text-muted-foreground text-xs">영문자로 시작, 영문자/숫자/언더스코어만 사용 가능</p>
|
<p className="text-muted-foreground text-xs">영문자로 시작, 영문자/숫자/언더스코어만 사용 가능</p>
|
||||||
|
|
@ -369,7 +369,7 @@ export function CreateTableModal({
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label>
|
<Label>
|
||||||
컬럼 정의 <span className="text-red-500">*</span>
|
컬럼 정의 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={addColumn} disabled={loading}>
|
<Button type="button" variant="outline" size="sm" onClick={addColumn} disabled={loading}>
|
||||||
<Plus className="mr-1 h-4 w-4" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
|
|
@ -440,7 +440,7 @@ export function CreateTableModal({
|
||||||
<div className="font-medium">경고:</div>
|
<div className="font-medium">경고:</div>
|
||||||
<ul className="list-inside list-disc space-y-1">
|
<ul className="list-inside list-disc space-y-1">
|
||||||
{validationResult.warnings.map((warning: string, index: number) => (
|
{validationResult.warnings.map((warning: string, index: number) => (
|
||||||
<li key={index} className="text-sm text-orange-600">
|
<li key={index} className="text-sm text-amber-600">
|
||||||
{warning}
|
{warning}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -471,7 +471,7 @@ export function CreateTableModal({
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreateTable}
|
onClick={handleCreateTable}
|
||||||
disabled={!isFormValid || loading || (validationResult && !validationResult.isValid)}
|
disabled={!isFormValid || loading || (validationResult && !validationResult.isValid)}
|
||||||
className="bg-green-600 hover:bg-green-700"
|
className="bg-emerald-600 hover:bg-green-700"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -276,11 +276,11 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{log.success ? (
|
{log.success ? (
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
|
||||||
) : (
|
) : (
|
||||||
<XCircle className="h-4 w-4 text-destructive" />
|
<XCircle className="h-4 w-4 text-destructive" />
|
||||||
)}
|
)}
|
||||||
<span className={log.success ? "text-green-600" : "text-destructive"}>
|
<span className={log.success ? "text-emerald-600" : "text-destructive"}>
|
||||||
{log.success ? "성공" : "실패"}
|
{log.success ? "성공" : "실패"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -323,7 +323,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
|
||||||
<CardTitle className="text-sm font-medium">성공</CardTitle>
|
<CardTitle className="text-sm font-medium">성공</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-green-600">{statistics.successfulExecutions}</div>
|
<div className="text-2xl font-bold text-emerald-600">{statistics.successfulExecutions}</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -484,7 +484,7 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
|
||||||
<div
|
<div
|
||||||
className={`rounded-md border p-3 text-sm ${
|
className={`rounded-md border p-3 text-sm ${
|
||||||
testResult.success
|
testResult.success
|
||||||
? "border-green-200 bg-green-50 text-green-800"
|
? "border-emerald-200 bg-emerald-50 text-emerald-800"
|
||||||
: "border-destructive/20 bg-destructive/10 text-red-800"
|
: "border-destructive/20 bg-destructive/10 text-red-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -527,7 +527,7 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{showAdvanced && (
|
{showAdvanced && (
|
||||||
<div className="space-y-4 border-l-2 border-gray-200 pl-6">
|
<div className="space-y-4 border-l-2 border-border pl-6">
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="connection_timeout">연결 타임아웃 (초)</Label>
|
<Label htmlFor="connection_timeout">연결 타임아웃 (초)</Label>
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export function HeadersManager({ headers, onChange }: HeadersManagerProps) {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeHeader(index)}
|
onClick={() => removeHeader(index)}
|
||||||
className="h-8 w-8 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
|
className="h-8 w-8 p-0 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -127,12 +127,12 @@ export function HeadersManager({ headers, onChange }: HeadersManagerProps) {
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-md border border-dashed p-4 text-center text-sm text-gray-500">
|
<div className="rounded-md border border-dashed p-4 text-center text-sm text-muted-foreground">
|
||||||
헤더가 없습니다. 헤더 추가 버튼을 클릭하여 추가하세요.
|
헤더가 없습니다. 헤더 추가 버튼을 클릭하여 추가하세요.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
* 공통으로 사용할 HTTP 헤더를 설정합니다. 인증 헤더는 별도의 인증 설정에서 관리됩니다.
|
* 공통으로 사용할 HTTP 헤더를 설정합니다. 인증 헤더는 별도의 인증 설정에서 관리됩니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -239,30 +239,30 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
||||||
<div className="mb-6 flex items-center justify-center">
|
<div className="mb-6 flex items-center justify-center">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 ${step === "basic" ? "text-primary" : step === "template" || step === "advanced" ? "text-green-600" : "text-gray-400"}`}
|
className={`flex items-center gap-2 ${step === "basic" ? "text-primary" : step === "template" || step === "advanced" ? "text-emerald-600" : "text-muted-foreground/70"}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "basic" ? "bg-primary/20 text-primary" : step === "template" || step === "advanced" ? "bg-green-100 text-green-600" : "bg-gray-100"}`}
|
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "basic" ? "bg-primary/20 text-primary" : step === "template" || step === "advanced" ? "bg-emerald-100 text-emerald-600" : "bg-muted"}`}
|
||||||
>
|
>
|
||||||
1
|
1
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium">기본 정보</span>
|
<span className="text-sm font-medium">기본 정보</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-px w-8 bg-gray-300" />
|
<div className="h-px w-8 bg-muted/60" />
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 ${step === "template" ? "text-primary" : step === "advanced" ? "text-green-600" : "text-gray-400"}`}
|
className={`flex items-center gap-2 ${step === "template" ? "text-primary" : step === "advanced" ? "text-emerald-600" : "text-muted-foreground/70"}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "template" ? "bg-primary/20 text-primary" : step === "advanced" ? "bg-green-100 text-green-600" : "bg-gray-100"}`}
|
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "template" ? "bg-primary/20 text-primary" : step === "advanced" ? "bg-emerald-100 text-emerald-600" : "bg-muted"}`}
|
||||||
>
|
>
|
||||||
2
|
2
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium">템플릿 선택</span>
|
<span className="text-sm font-medium">템플릿 선택</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-px w-8 bg-gray-300" />
|
<div className="h-px w-8 bg-muted/60" />
|
||||||
<div className={`flex items-center gap-2 ${step === "advanced" ? "text-primary" : "text-gray-400"}`}>
|
<div className={`flex items-center gap-2 ${step === "advanced" ? "text-primary" : "text-muted-foreground/70"}`}>
|
||||||
<div
|
<div
|
||||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "advanced" ? "bg-primary/20 text-primary" : "bg-gray-100"}`}
|
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "advanced" ? "bg-primary/20 text-primary" : "bg-muted"}`}
|
||||||
>
|
>
|
||||||
3
|
3
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -305,7 +305,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
||||||
<Card
|
<Card
|
||||||
key={category.id}
|
key={category.id}
|
||||||
className={`cursor-pointer transition-all ${
|
className={`cursor-pointer transition-all ${
|
||||||
formData.category === category.id ? "bg-accent ring-2 ring-blue-500" : "hover:bg-gray-50"
|
formData.category === category.id ? "bg-accent ring-2 ring-ring" : "hover:bg-muted"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setFormData((prev) => ({ ...prev, category: category.id }))}
|
onClick={() => setFormData((prev) => ({ ...prev, category: category.id }))}
|
||||||
>
|
>
|
||||||
|
|
@ -314,7 +314,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
||||||
<IconComponent className="h-5 w-5 text-muted-foreground" />
|
<IconComponent className="h-5 w-5 text-muted-foreground" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{category.name}</div>
|
<div className="font-medium">{category.name}</div>
|
||||||
<div className="text-xs text-gray-500">{category.description}</div>
|
<div className="text-xs text-muted-foreground">{category.description}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
@ -341,13 +341,13 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>레이아웃 템플릿 *</Label>
|
<Label>레이아웃 템플릿 *</Label>
|
||||||
<p className="mb-3 text-sm text-gray-500">원하는 레이아웃 구조를 선택하세요</p>
|
<p className="mb-3 text-sm text-muted-foreground">원하는 레이아웃 구조를 선택하세요</p>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{LAYOUT_TEMPLATES.map((template) => (
|
{LAYOUT_TEMPLATES.map((template) => (
|
||||||
<Card
|
<Card
|
||||||
key={template.id}
|
key={template.id}
|
||||||
className={`cursor-pointer transition-all ${
|
className={`cursor-pointer transition-all ${
|
||||||
formData.template === template.id ? "bg-accent ring-2 ring-blue-500" : "hover:bg-gray-50"
|
formData.template === template.id ? "bg-accent ring-2 ring-ring" : "hover:bg-muted"
|
||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
|
|
@ -364,8 +364,8 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
||||||
<Badge variant="secondary">{template.zones}개 영역</Badge>
|
<Badge variant="secondary">{template.zones}개 영역</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">{template.description}</div>
|
<div className="text-sm text-muted-foreground">{template.description}</div>
|
||||||
<div className="text-xs text-gray-500">예: {template.example}</div>
|
<div className="text-xs text-muted-foreground">예: {template.example}</div>
|
||||||
<div className="rounded bg-gray-100 p-2 text-center font-mono text-xs">{template.icon}</div>
|
<div className="rounded bg-muted p-2 text-center font-mono text-xs">{template.icon}</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -418,7 +418,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm text-gray-500">개 영역</span>
|
<span className="text-sm text-muted-foreground">개 영역</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -428,17 +428,17 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{generationResult ? (
|
{generationResult ? (
|
||||||
<Alert
|
<Alert
|
||||||
className={generationResult.success ? "border-green-200 bg-green-50" : "border-destructive/20 bg-destructive/10"}
|
className={generationResult.success ? "border-emerald-200 bg-emerald-50" : "border-destructive/20 bg-destructive/10"}
|
||||||
>
|
>
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4" />
|
||||||
<AlertDescription className={generationResult.success ? "text-green-800" : "text-red-800"}>
|
<AlertDescription className={generationResult.success ? "text-emerald-800" : "text-red-800"}>
|
||||||
{generationResult.message}
|
{generationResult.message}
|
||||||
{generationResult.success && generationResult.files && (
|
{generationResult.success && generationResult.files && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<div className="text-sm font-medium">생성된 파일들:</div>
|
<div className="text-sm font-medium">생성된 파일들:</div>
|
||||||
<ul className="mt-1 space-y-1 text-xs">
|
<ul className="mt-1 space-y-1 text-xs">
|
||||||
{generationResult.files.map((file, index) => (
|
{generationResult.files.map((file, index) => (
|
||||||
<li key={index} className="text-green-700">
|
<li key={index} className="text-emerald-700">
|
||||||
• {file}
|
• {file}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -748,7 +748,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{!isEdit && level !== 1 && (
|
{!isEdit && level !== 1 && (
|
||||||
<p className="text-xs text-gray-500">{getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SUBMENU_NOTE)}</p>
|
<p className="text-xs text-muted-foreground">{getText(MENU_MANAGEMENT_KEYS.FORM_COMPANY_SUBMENU_NOTE)}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -817,7 +817,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="font-medium">{key.langKey}</div>
|
<div className="font-medium">{key.langKey}</div>
|
||||||
{key.description && <div className="text-xs text-gray-500">{key.description}</div>}
|
{key.description && <div className="text-xs text-muted-foreground">{key.description}</div>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -825,7 +825,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{selectedLangKeyInfo && (
|
{selectedLangKeyInfo && (
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECTED)
|
{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECTED)
|
||||||
.replace("{key}", selectedLangKeyInfo.langKey)
|
.replace("{key}", selectedLangKeyInfo.langKey)
|
||||||
.replace("{description}", selectedLangKeyInfo.description)}
|
.replace("{description}", selectedLangKeyInfo.description)}
|
||||||
|
|
@ -896,7 +896,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
{/* 검색 입력 */}
|
{/* 검색 입력 */}
|
||||||
<div className="sticky top-0 border-b bg-white p-2">
|
<div className="sticky top-0 border-b bg-white p-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
<Search className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="화면 검색..."
|
placeholder="화면 검색..."
|
||||||
value={screenSearchText}
|
value={screenSearchText}
|
||||||
|
|
@ -918,14 +918,14 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
<div
|
<div
|
||||||
key={`screen-${screen.screenId || screen.id || index}-${screen.screenCode || index}`}
|
key={`screen-${screen.screenId || screen.id || index}-${screen.screenCode || index}`}
|
||||||
onClick={() => handleScreenSelect(screen)}
|
onClick={() => handleScreenSelect(screen)}
|
||||||
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-gray-100"
|
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-muted"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">{screen.screenName}</div>
|
<div className="text-sm font-medium">{screen.screenName}</div>
|
||||||
<div className="text-xs text-gray-500">{screen.screenCode}</div>
|
<div className="text-xs text-muted-foreground">{screen.screenCode}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">ID: {screen.screenId || screen.id || "N/A"}</div>
|
<div className="text-xs text-muted-foreground/70">ID: {screen.screenId || screen.id || "N/A"}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -933,7 +933,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
(screen) =>
|
(screen) =>
|
||||||
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
|
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
|
||||||
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
|
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
|
||||||
).length === 0 && <div className="px-3 py-2 text-sm text-gray-500">검색 결과가 없습니다.</div>}
|
).length === 0 && <div className="px-3 py-2 text-sm text-muted-foreground">검색 결과가 없습니다.</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -942,7 +942,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
{/* 선택된 화면 정보 표시 */}
|
{/* 선택된 화면 정보 표시 */}
|
||||||
{selectedScreen && (
|
{selectedScreen && (
|
||||||
<div className="bg-accent rounded-md border p-3">
|
<div className="bg-accent rounded-md border p-3">
|
||||||
<div className="text-sm font-medium text-blue-900">{selectedScreen.screenName}</div>
|
<div className="text-sm font-medium text-primary">{selectedScreen.screenName}</div>
|
||||||
<div className="text-primary text-xs">코드: {selectedScreen.screenCode}</div>
|
<div className="text-primary text-xs">코드: {selectedScreen.screenCode}</div>
|
||||||
<div className="text-primary text-xs">생성된 URL: {formData.menuUrl}</div>
|
<div className="text-primary text-xs">생성된 URL: {formData.menuUrl}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -971,7 +971,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
{/* 검색창 */}
|
{/* 검색창 */}
|
||||||
<div className="border-b p-2">
|
<div className="border-b p-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute top-2.5 left-2 h-4 w-4 text-gray-400" />
|
<Search className="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground/70" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="대시보드 검색..."
|
placeholder="대시보드 검색..."
|
||||||
|
|
@ -995,13 +995,13 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
<div
|
<div
|
||||||
key={dashboard.id}
|
key={dashboard.id}
|
||||||
onClick={() => handleDashboardSelect(dashboard)}
|
onClick={() => handleDashboardSelect(dashboard)}
|
||||||
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-gray-100"
|
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-muted"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">{dashboard.title}</div>
|
<div className="text-sm font-medium">{dashboard.title}</div>
|
||||||
{dashboard.description && (
|
{dashboard.description && (
|
||||||
<div className="text-xs text-gray-500">{dashboard.description}</div>
|
<div className="text-xs text-muted-foreground">{dashboard.description}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1012,7 +1012,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
dashboard.title.toLowerCase().includes(dashboardSearchText.toLowerCase()) ||
|
dashboard.title.toLowerCase().includes(dashboardSearchText.toLowerCase()) ||
|
||||||
(dashboard.description &&
|
(dashboard.description &&
|
||||||
dashboard.description.toLowerCase().includes(dashboardSearchText.toLowerCase())),
|
dashboard.description.toLowerCase().includes(dashboardSearchText.toLowerCase())),
|
||||||
).length === 0 && <div className="px-3 py-2 text-sm text-gray-500">검색 결과가 없습니다.</div>}
|
).length === 0 && <div className="px-3 py-2 text-sm text-muted-foreground">검색 결과가 없습니다.</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1021,7 +1021,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
{/* 선택된 대시보드 정보 표시 */}
|
{/* 선택된 대시보드 정보 표시 */}
|
||||||
{selectedDashboard && (
|
{selectedDashboard && (
|
||||||
<div className="bg-accent rounded-md border p-3">
|
<div className="bg-accent rounded-md border p-3">
|
||||||
<div className="text-sm font-medium text-blue-900">{selectedDashboard.title}</div>
|
<div className="text-sm font-medium text-primary">{selectedDashboard.title}</div>
|
||||||
{selectedDashboard.description && (
|
{selectedDashboard.description && (
|
||||||
<div className="text-primary text-xs">설명: {selectedDashboard.description}</div>
|
<div className="text-primary text-xs">설명: {selectedDashboard.description}</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -745,7 +745,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
||||||
<span>생성 (C)</span>
|
<span>생성 (C)</span>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onCheckedChange={(checked) => handleSelectAll("createYn", checked as boolean)}
|
onCheckedChange={(checked) => handleSelectAll("createYn", checked as boolean)}
|
||||||
className="data-[state=checked]:bg-green-600"
|
className="data-[state=checked]:bg-emerald-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
@ -754,7 +754,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
||||||
<span>조회 (R)</span>
|
<span>조회 (R)</span>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onCheckedChange={(checked) => handleSelectAll("readYn", checked as boolean)}
|
onCheckedChange={(checked) => handleSelectAll("readYn", checked as boolean)}
|
||||||
className="data-[state=checked]:bg-blue-600"
|
className="data-[state=checked]:bg-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
@ -772,7 +772,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
||||||
<span>삭제 (D)</span>
|
<span>삭제 (D)</span>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onCheckedChange={(checked) => handleSelectAll("deleteYn", checked as boolean)}
|
onCheckedChange={(checked) => handleSelectAll("deleteYn", checked as boolean)}
|
||||||
className="data-[state=checked]:bg-red-600"
|
className="data-[state=checked]:bg-destructive"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
@ -834,7 +834,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={menu.createYn === "Y"}
|
checked={menu.createYn === "Y"}
|
||||||
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "createYn", checked as boolean)}
|
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "createYn", checked as boolean)}
|
||||||
className="data-[state=checked]:bg-green-600"
|
className="data-[state=checked]:bg-emerald-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
|
@ -842,7 +842,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={menu.readYn === "Y"}
|
checked={menu.readYn === "Y"}
|
||||||
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "readYn", checked as boolean)}
|
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "readYn", checked as boolean)}
|
||||||
className="data-[state=checked]:bg-blue-600"
|
className="data-[state=checked]:bg-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
|
@ -858,7 +858,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={menu.deleteYn === "Y"}
|
checked={menu.deleteYn === "Y"}
|
||||||
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "deleteYn", checked as boolean)}
|
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "deleteYn", checked as boolean)}
|
||||||
className="data-[state=checked]:bg-red-600"
|
className="data-[state=checked]:bg-destructive"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -564,7 +564,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
{testResult && (
|
{testResult && (
|
||||||
<div
|
<div
|
||||||
className={`rounded-md border p-4 ${
|
className={`rounded-md border p-4 ${
|
||||||
testResult.success ? "border-green-200 bg-green-50" : "border-red-200 bg-red-50"
|
testResult.success ? "border-emerald-200 bg-emerald-50" : "border-destructive/20 bg-destructive/10"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
|
@ -572,17 +572,17 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
{testResult.success ? "성공" : "실패"}
|
{testResult.success ? "성공" : "실패"}
|
||||||
</Badge>
|
</Badge>
|
||||||
{testResult.response_time && (
|
{testResult.response_time && (
|
||||||
<span className="text-sm text-gray-600">응답 시간: {testResult.response_time}ms</span>
|
<span className="text-sm text-muted-foreground">응답 시간: {testResult.response_time}ms</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm">{testResult.message}</p>
|
<p className="text-sm">{testResult.message}</p>
|
||||||
|
|
||||||
{testResult.status_code && (
|
{testResult.status_code && (
|
||||||
<p className="mt-1 text-xs text-gray-500">상태 코드: {testResult.status_code}</p>
|
<p className="mt-1 text-xs text-muted-foreground">상태 코드: {testResult.status_code}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{testResult.error_details && <p className="mt-2 text-xs text-red-600">{testResult.error_details}</p>}
|
{testResult.error_details && <p className="mt-2 text-xs text-destructive">{testResult.error_details}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,9 @@ export function RoleDeleteModal({ isOpen, onClose, onSuccess, role }: RoleDelete
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 경고 메시지 */}
|
{/* 경고 메시지 */}
|
||||||
<div className="rounded-lg border border-orange-300 bg-orange-50 p-4">
|
<div className="rounded-lg border border-orange-300 bg-amber-50 p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertTriangle className="mt-0.5 h-5 w-5 flex-shrink-0 text-orange-600" />
|
<AlertTriangle className="mt-0.5 h-5 w-5 flex-shrink-0 text-amber-600" />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-semibold text-orange-900">정말로 삭제하시겠습니까?</p>
|
<p className="text-sm font-semibold text-orange-900">정말로 삭제하시겠습니까?</p>
|
||||||
<p className="text-xs text-orange-800">
|
<p className="text-xs text-orange-800">
|
||||||
|
|
@ -124,7 +124,7 @@ export function RoleDeleteModal({ isOpen, onClose, onSuccess, role }: RoleDelete
|
||||||
<div
|
<div
|
||||||
className={`rounded-lg border p-3 text-sm ${
|
className={`rounded-lg border p-3 text-sm ${
|
||||||
alertType === "success"
|
alertType === "success"
|
||||||
? "border-green-300 bg-green-50 text-green-800"
|
? "border-green-300 bg-emerald-50 text-emerald-800"
|
||||||
: "border-destructive/50 bg-destructive/10 text-destructive"
|
: "border-destructive/50 bg-destructive/10 text-destructive"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
||||||
{/* 권한 그룹명 */}
|
{/* 권한 그룹명 */}
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="authName" className="text-xs sm:text-sm">
|
<Label htmlFor="authName" className="text-xs sm:text-sm">
|
||||||
권한 그룹명 <span className="text-red-500">*</span>
|
권한 그룹명 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="authName"
|
id="authName"
|
||||||
|
|
@ -210,7 +210,7 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
||||||
{/* 권한 코드 */}
|
{/* 권한 코드 */}
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="authCode" className="text-xs sm:text-sm">
|
<Label htmlFor="authCode" className="text-xs sm:text-sm">
|
||||||
권한 코드 <span className="text-red-500">*</span>
|
권한 코드 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="authCode"
|
id="authCode"
|
||||||
|
|
@ -243,7 +243,7 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="companyCode" className="text-xs sm:text-sm">
|
<Label htmlFor="companyCode" className="text-xs sm:text-sm">
|
||||||
회사 <span className="text-red-500">*</span>
|
회사 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
{isSuperAdmin ? (
|
{isSuperAdmin ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -345,10 +345,10 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
||||||
<div
|
<div
|
||||||
className={`rounded-lg border p-3 text-sm ${
|
className={`rounded-lg border p-3 text-sm ${
|
||||||
alertType === "success"
|
alertType === "success"
|
||||||
? "border-green-300 bg-green-50 text-green-800"
|
? "border-green-300 bg-emerald-50 text-emerald-800"
|
||||||
: alertType === "error"
|
: alertType === "error"
|
||||||
? "border-destructive/50 bg-destructive/10 text-destructive"
|
? "border-destructive/50 bg-destructive/10 text-destructive"
|
||||||
: "border-blue-300 bg-blue-50 text-blue-800"
|
: "border-primary/40 bg-primary/10 text-primary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedMenu && (
|
{selectedMenu && (
|
||||||
<div className="rounded-lg border bg-gray-50 p-4">
|
<div className="rounded-lg border bg-muted p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium">
|
<h3 className="font-medium">
|
||||||
|
|
@ -267,7 +267,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
||||||
<Monitor className="h-5 w-5" />
|
<Monitor className="h-5 w-5" />
|
||||||
할당된 화면 ({assignedScreens.length}개)
|
할당된 화면 ({assignedScreens.length}개)
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button onClick={openAssignDialog} className="bg-blue-600 hover:bg-blue-700">
|
<Button onClick={openAssignDialog} className="bg-primary hover:bg-primary/90">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
화면 할당
|
화면 할당
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -275,15 +275,15 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="py-8 text-center text-gray-500">로딩 중...</div>
|
<div className="py-8 text-center text-muted-foreground">로딩 중...</div>
|
||||||
) : assignedScreens.length === 0 ? (
|
) : assignedScreens.length === 0 ? (
|
||||||
<div className="py-8 text-center text-gray-500">할당된 화면이 없습니다. 화면을 할당해보세요.</div>
|
<div className="py-8 text-center text-muted-foreground">할당된 화면이 없습니다. 화면을 할당해보세요.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{assignedScreens.map((screen) => (
|
{assignedScreens.map((screen) => (
|
||||||
<div
|
<div
|
||||||
key={screen.screenId}
|
key={screen.screenId}
|
||||||
className="flex items-center justify-between rounded-lg border p-4 hover:bg-gray-50"
|
className="flex items-center justify-between rounded-lg border p-4 hover:bg-muted"
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
@ -298,7 +298,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
테이블: {screen.tableName} | 생성일: {screen.createdDate.toLocaleDateString()}
|
테이블: {screen.tableName} | 생성일: {screen.createdDate.toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
{screen.description && <p className="mt-1 text-sm text-gray-500">{screen.description}</p>}
|
{screen.description && <p className="mt-1 text-sm text-muted-foreground">{screen.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -307,7 +307,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
||||||
setSelectedScreen(screen);
|
setSelectedScreen(screen);
|
||||||
setShowUnassignDialog(true);
|
setShowUnassignDialog(true);
|
||||||
}}
|
}}
|
||||||
className="text-destructive hover:text-red-700"
|
className="text-destructive hover:text-destructive"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -330,7 +330,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 검색 */}
|
{/* 검색 */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-muted-foreground/70" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="화면명 또는 코드로 검색..."
|
placeholder="화면명 또는 코드로 검색..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
|
|
@ -342,13 +342,13 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
||||||
{/* 화면 목록 */}
|
{/* 화면 목록 */}
|
||||||
<div className="max-h-64 space-y-2 overflow-y-auto">
|
<div className="max-h-64 space-y-2 overflow-y-auto">
|
||||||
{filteredAvailableScreens.length === 0 ? (
|
{filteredAvailableScreens.length === 0 ? (
|
||||||
<div className="py-4 text-center text-gray-500">할당 가능한 화면이 없습니다.</div>
|
<div className="py-4 text-center text-muted-foreground">할당 가능한 화면이 없습니다.</div>
|
||||||
) : (
|
) : (
|
||||||
filteredAvailableScreens.map((screen) => (
|
filteredAvailableScreens.map((screen) => (
|
||||||
<div
|
<div
|
||||||
key={screen.screenId}
|
key={screen.screenId}
|
||||||
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
|
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
|
||||||
selectedScreen?.screenId === screen.screenId ? "border-primary bg-accent" : "hover:bg-gray-50"
|
selectedScreen?.screenId === screen.screenId ? "border-primary bg-accent" : "hover:bg-muted"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSelectedScreen(screen)}
|
onClick={() => setSelectedScreen(screen)}
|
||||||
>
|
>
|
||||||
|
|
@ -385,7 +385,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel onClick={() => setSelectedScreen(null)}>취소</AlertDialogCancel>
|
<AlertDialogCancel onClick={() => setSelectedScreen(null)}>취소</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleUnassignScreen} className="bg-red-600 hover:bg-red-700">
|
<AlertDialogAction onClick={handleUnassignScreen} className="bg-destructive hover:bg-red-700">
|
||||||
할당 해제
|
할당 해제
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export function SortableCodeItem({
|
||||||
}}
|
}}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
className="text-muted-foreground hover:text-foreground -ml-1 flex h-5 w-5 items-center justify-center rounded transition-colors hover:bg-gray-100"
|
className="text-muted-foreground hover:text-foreground -ml-1 flex h-5 w-5 items-center justify-center rounded transition-colors hover:bg-muted"
|
||||||
title={isExpanded ? "접기" : "펼치기"}
|
title={isExpanded ? "접기" : "펼치기"}
|
||||||
>
|
>
|
||||||
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||||
|
|
@ -135,12 +135,12 @@ export function SortableCodeItem({
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{depth === 2 && (
|
{depth === 2 && (
|
||||||
<Badge variant="outline" className="bg-blue-50 px-1.5 py-0 text-[10px] text-blue-600">
|
<Badge variant="outline" className="bg-primary/10 px-1.5 py-0 text-[10px] text-primary">
|
||||||
중분류
|
중분류
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{depth === 3 && (
|
{depth === 3 && (
|
||||||
<Badge variant="outline" className="bg-green-50 px-1.5 py-0 text-[10px] text-green-600">
|
<Badge variant="outline" className="bg-emerald-50 px-1.5 py-0 text-[10px] text-emerald-600">
|
||||||
소분류
|
소분류
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
@ -196,7 +196,7 @@ export function SortableCodeItem({
|
||||||
onAddChild();
|
onAddChild();
|
||||||
}}
|
}}
|
||||||
title="하위 코드 추가"
|
title="하위 코드 추가"
|
||||||
className="text-blue-600 hover:bg-blue-50 hover:text-blue-700"
|
className="text-primary hover:bg-primary/10 hover:text-primary"
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -102,11 +102,11 @@ export function TableLogViewer({ tableName, open, onOpenChange }: TableLogViewer
|
||||||
const getOperationBadge = (type: string) => {
|
const getOperationBadge = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "INSERT":
|
case "INSERT":
|
||||||
return <Badge className="bg-green-500">추가</Badge>;
|
return <Badge className="bg-emerald-500">추가</Badge>;
|
||||||
case "UPDATE":
|
case "UPDATE":
|
||||||
return <Badge className="bg-blue-500">수정</Badge>;
|
return <Badge className="bg-primary">수정</Badge>;
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
return <Badge className="bg-red-500">삭제</Badge>;
|
return <Badge className="bg-destructive">삭제</Badge>;
|
||||||
default:
|
default:
|
||||||
return <Badge>{type}</Badge>;
|
return <Badge>{type}</Badge>;
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +150,7 @@ export function TableLogViewer({ tableName, open, onOpenChange }: TableLogViewer
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
|
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm text-gray-600">작업 유형</label>
|
<label className="mb-1 block text-sm text-muted-foreground">작업 유형</label>
|
||||||
{/* Radix UI Select v2.x: 빈 문자열 value="" 금지 → "__all__" 사용 */}
|
{/* Radix UI Select v2.x: 빈 문자열 value="" 금지 → "__all__" 사용 */}
|
||||||
<Select
|
<Select
|
||||||
value={operationType || "__all__"}
|
value={operationType || "__all__"}
|
||||||
|
|
@ -169,22 +169,22 @@ export function TableLogViewer({ tableName, open, onOpenChange }: TableLogViewer
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm text-gray-600">시작 날짜</label>
|
<label className="mb-1 block text-sm text-muted-foreground">시작 날짜</label>
|
||||||
<Input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
|
<Input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm text-gray-600">종료 날짜</label>
|
<label className="mb-1 block text-sm text-muted-foreground">종료 날짜</label>
|
||||||
<Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
|
<Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm text-gray-600">변경자</label>
|
<label className="mb-1 block text-sm text-muted-foreground">변경자</label>
|
||||||
<Input placeholder="사용자 ID" value={changedBy} onChange={(e) => setChangedBy(e.target.value)} />
|
<Input placeholder="사용자 ID" value={changedBy} onChange={(e) => setChangedBy(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm text-gray-600">원본 ID</label>
|
<label className="mb-1 block text-sm text-muted-foreground">원본 ID</label>
|
||||||
<Input placeholder="레코드 ID" value={originalId} onChange={(e) => setOriginalId(e.target.value)} />
|
<Input placeholder="레코드 ID" value={originalId} onChange={(e) => setOriginalId(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -204,7 +204,7 @@ export function TableLogViewer({ tableName, open, onOpenChange }: TableLogViewer
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</div>
|
</div>
|
||||||
) : logs.length === 0 ? (
|
) : logs.length === 0 ? (
|
||||||
<div className="flex h-64 items-center justify-center text-gray-500">변경 이력이 없습니다.</div>
|
<div className="flex h-64 items-center justify-center text-muted-foreground">변경 이력이 없습니다.</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
|
@ -243,7 +243,7 @@ export function TableLogViewer({ tableName, open, onOpenChange }: TableLogViewer
|
||||||
|
|
||||||
{/* 페이지네이션 */}
|
{/* 페이지네이션 */}
|
||||||
<div className="flex items-center justify-between border-t pt-4">
|
<div className="flex items-center justify-between border-t pt-4">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-muted-foreground">
|
||||||
전체 {total}건 (페이지 {page} / {totalPages})
|
전체 {total}건 (페이지 {page} / {totalPages})
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
|
||||||
|
|
@ -157,12 +157,12 @@ export function TemplateImportExport({ onTemplateImported }: TemplateImportExpor
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-8 text-center transition-colors hover:border-gray-400"
|
className="cursor-pointer rounded-lg border-2 border-dashed border-input p-8 text-center transition-colors hover:border-input"
|
||||||
onClick={triggerFileSelect}
|
onClick={triggerFileSelect}
|
||||||
>
|
>
|
||||||
<Upload className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
<Upload className="mx-auto mb-4 h-12 w-12 text-muted-foreground/70" />
|
||||||
<p className="mb-2 text-lg font-medium text-gray-900">템플릿 JSON 파일을 선택하세요</p>
|
<p className="mb-2 text-lg font-medium text-foreground">템플릿 JSON 파일을 선택하세요</p>
|
||||||
<p className="text-sm text-gray-500">또는 아래에 JSON 내용을 직접 입력하세요</p>
|
<p className="text-sm text-muted-foreground">또는 아래에 JSON 내용을 직접 입력하세요</p>
|
||||||
</div>
|
</div>
|
||||||
<input ref={fileInputRef} type="file" accept=".json" onChange={handleFileUpload} className="hidden" />
|
<input ref={fileInputRef} type="file" accept=".json" onChange={handleFileUpload} className="hidden" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
@ -196,7 +196,7 @@ export function TemplateImportExport({ onTemplateImported }: TemplateImportExpor
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center text-lg">
|
<CardTitle className="flex items-center text-lg">
|
||||||
<CheckCircle className="mr-2 h-5 w-5 text-green-600" />
|
<CheckCircle className="mr-2 h-5 w-5 text-emerald-600" />
|
||||||
3. 템플릿 미리보기
|
3. 템플릿 미리보기
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
|
||||||
|
|
@ -54,28 +54,28 @@ export function UserAuthEditModal({ isOpen, onClose, onSuccess, user }: UserAuth
|
||||||
label: "회사 관리자",
|
label: "회사 관리자",
|
||||||
description: "자기 회사 데이터 및 사용자 관리 가능",
|
description: "자기 회사 데이터 및 사용자 관리 가능",
|
||||||
icon: <Building2 className="h-4 w-4" />,
|
icon: <Building2 className="h-4 w-4" />,
|
||||||
color: "text-blue-600",
|
color: "text-primary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "USER",
|
value: "USER",
|
||||||
label: "일반 사용자",
|
label: "일반 사용자",
|
||||||
description: "자기 회사 데이터 조회/수정만 가능",
|
description: "자기 회사 데이터 조회/수정만 가능",
|
||||||
icon: <User className="h-4 w-4" />,
|
icon: <User className="h-4 w-4" />,
|
||||||
color: "text-gray-600",
|
color: "text-muted-foreground",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "GUEST",
|
value: "GUEST",
|
||||||
label: "게스트",
|
label: "게스트",
|
||||||
description: "제한된 조회 권한",
|
description: "제한된 조회 권한",
|
||||||
icon: <Users className="h-4 w-4" />,
|
icon: <Users className="h-4 w-4" />,
|
||||||
color: "text-green-600",
|
color: "text-emerald-600",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "PARTNER",
|
value: "PARTNER",
|
||||||
label: "협력업체",
|
label: "협력업체",
|
||||||
description: "협력업체 전용 권한",
|
description: "협력업체 전용 권한",
|
||||||
icon: <Shield className="h-4 w-4" />,
|
icon: <Shield className="h-4 w-4" />,
|
||||||
color: "text-orange-600",
|
color: "text-amber-600",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ export function UserAuthEditModal({ isOpen, onClose, onSuccess, user }: UserAuth
|
||||||
{/* 권한 선택 */}
|
{/* 권한 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="userType" className="text-sm font-medium">
|
<Label htmlFor="userType" className="text-sm font-medium">
|
||||||
새로운 권한 <span className="text-red-500">*</span>
|
새로운 권한 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={selectedUserType} onValueChange={setSelectedUserType}>
|
<Select value={selectedUserType} onValueChange={setSelectedUserType}>
|
||||||
<SelectTrigger className="h-10">
|
<SelectTrigger className="h-10">
|
||||||
|
|
@ -180,9 +180,9 @@ export function UserAuthEditModal({ isOpen, onClose, onSuccess, user }: UserAuth
|
||||||
|
|
||||||
{/* SUPER_ADMIN 경고 */}
|
{/* SUPER_ADMIN 경고 */}
|
||||||
{showConfirmation && selectedUserType === "SUPER_ADMIN" && (
|
{showConfirmation && selectedUserType === "SUPER_ADMIN" && (
|
||||||
<div className="rounded-lg border border-orange-300 bg-orange-50 p-4">
|
<div className="rounded-lg border border-orange-300 bg-amber-50 p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertTriangle className="mt-0.5 h-5 w-5 flex-shrink-0 text-orange-600" />
|
<AlertTriangle className="mt-0.5 h-5 w-5 flex-shrink-0 text-amber-600" />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-semibold text-orange-900">최고 관리자 권한 부여 경고</p>
|
<p className="text-sm font-semibold text-orange-900">최고 관리자 권한 부여 경고</p>
|
||||||
<p className="text-xs text-orange-800">
|
<p className="text-xs text-orange-800">
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertMod
|
||||||
const getTypeColor = () => {
|
const getTypeColor = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "success":
|
case "success":
|
||||||
return "text-green-600";
|
return "text-emerald-600";
|
||||||
case "error":
|
case "error":
|
||||||
return "text-destructive";
|
return "text-destructive";
|
||||||
default:
|
default:
|
||||||
|
|
@ -446,7 +446,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserF
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="userId" className="text-sm font-medium">
|
<Label htmlFor="userId" className="text-sm font-medium">
|
||||||
사용자 ID <span className="text-red-500">*</span>
|
사용자 ID <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
{isEditMode ? (
|
{isEditMode ? (
|
||||||
<Input id="userId" value={formData.userId} disabled className="bg-muted cursor-not-allowed" />
|
<Input id="userId" value={formData.userId} disabled className="bg-muted cursor-not-allowed" />
|
||||||
|
|
@ -474,7 +474,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserF
|
||||||
{/* 중복확인 결과 메시지 */}
|
{/* 중복확인 결과 메시지 */}
|
||||||
{duplicateCheckMessage && (
|
{duplicateCheckMessage && (
|
||||||
<div
|
<div
|
||||||
className={`mt-1 text-sm ${duplicateCheckType === "success" ? "text-green-600" : "text-destructive"}`}
|
className={`mt-1 text-sm ${duplicateCheckType === "success" ? "text-emerald-600" : "text-destructive"}`}
|
||||||
>
|
>
|
||||||
{duplicateCheckMessage}
|
{duplicateCheckMessage}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -485,7 +485,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserF
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="userName" className="text-sm font-medium">
|
<Label htmlFor="userName" className="text-sm font-medium">
|
||||||
사용자명 <span className="text-red-500">*</span>
|
사용자명 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="userName"
|
id="userName"
|
||||||
|
|
@ -501,7 +501,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserF
|
||||||
{!isEditMode && (
|
{!isEditMode && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="userPassword" className="text-sm font-medium">
|
<Label htmlFor="userPassword" className="text-sm font-medium">
|
||||||
비밀번호 <span className="text-red-500">*</span>
|
비밀번호 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -532,7 +532,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserF
|
||||||
{/* 회사 선택 */}
|
{/* 회사 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="companyCode" className="text-sm font-medium">
|
<Label htmlFor="companyCode" className="text-sm font-medium">
|
||||||
회사 <span className="text-red-500">*</span>
|
회사 <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
{isSuperAdmin ? (
|
{isSuperAdmin ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
|
||||||
<div className="space-y-4" onKeyDown={handleKeyDown}>
|
<div className="space-y-4" onKeyDown={handleKeyDown}>
|
||||||
{/* 대상 사용자 정보 */}
|
{/* 대상 사용자 정보 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-sm font-medium text-gray-700">
|
<Label className="text-sm font-medium text-foreground">
|
||||||
대상 사용자:{" "}
|
대상 사용자:{" "}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{userName} ({userId})
|
{userName} ({userId})
|
||||||
|
|
@ -197,10 +197,10 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
|
||||||
|
|
||||||
{/* 비밀번호 일치 여부 표시 */}
|
{/* 비밀번호 일치 여부 표시 */}
|
||||||
{showMismatchError && <p className="text-sm text-destructive">비밀번호가 일치하지 않습니다.</p>}
|
{showMismatchError && <p className="text-sm text-destructive">비밀번호가 일치하지 않습니다.</p>}
|
||||||
{isPasswordMatch && <p className="text-sm text-green-600">비밀번호가 일치합니다.</p>}
|
{isPasswordMatch && <p className="text-sm text-emerald-600">비밀번호가 일치합니다.</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-gray-500">* 비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.</div>
|
<div className="text-xs text-muted-foreground">* 비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex justify-end space-x-2">
|
<div className="mt-6 flex justify-end space-x-2">
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export function DriverListView({ drivers, config, isCompact = false }: DriverLis
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full overflow-auto">
|
<div className="h-full w-full overflow-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-border">
|
||||||
<thead className="sticky top-0 z-10 bg-muted">
|
<thead className="sticky top-0 z-10 bg-muted">
|
||||||
<tr>
|
<tr>
|
||||||
{visibleColumns.includes("status") && (
|
{visibleColumns.includes("status") && (
|
||||||
|
|
@ -96,7 +96,7 @@ export function DriverListView({ drivers, config, isCompact = false }: DriverLis
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200 bg-background">
|
<tbody className="divide-y divide-border bg-background">
|
||||||
{drivers.map((driver) => {
|
{drivers.map((driver) => {
|
||||||
const statusColors = getStatusColor(driver.status);
|
const statusColors = getStatusColor(driver.status);
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -180,16 +180,16 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
||||||
const renderIcon = (icon?: string, color?: string) => {
|
const renderIcon = (icon?: string, color?: string) => {
|
||||||
const colorClass =
|
const colorClass =
|
||||||
color === "blue"
|
color === "blue"
|
||||||
? "text-blue-600"
|
? "text-primary"
|
||||||
: color === "orange"
|
: color === "orange"
|
||||||
? "text-orange-600"
|
? "text-amber-600"
|
||||||
: color === "green"
|
: color === "green"
|
||||||
? "text-green-600"
|
? "text-emerald-600"
|
||||||
: color === "red"
|
: color === "red"
|
||||||
? "text-red-600"
|
? "text-destructive"
|
||||||
: color === "purple"
|
: color === "purple"
|
||||||
? "text-purple-600"
|
? "text-purple-600"
|
||||||
: "text-gray-600";
|
: "text-muted-foreground";
|
||||||
|
|
||||||
switch (icon) {
|
switch (icon) {
|
||||||
case "truck":
|
case "truck":
|
||||||
|
|
@ -209,16 +209,16 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
||||||
const renderFieldGroup = (group: FieldGroup, data: Record<string, any>) => {
|
const renderFieldGroup = (group: FieldGroup, data: Record<string, any>) => {
|
||||||
const colorClass =
|
const colorClass =
|
||||||
group.color === "blue"
|
group.color === "blue"
|
||||||
? "text-blue-600"
|
? "text-primary"
|
||||||
: group.color === "orange"
|
: group.color === "orange"
|
||||||
? "text-orange-600"
|
? "text-amber-600"
|
||||||
: group.color === "green"
|
: group.color === "green"
|
||||||
? "text-green-600"
|
? "text-emerald-600"
|
||||||
: group.color === "red"
|
: group.color === "red"
|
||||||
? "text-red-600"
|
? "text-destructive"
|
||||||
: group.color === "purple"
|
: group.color === "purple"
|
||||||
? "text-purple-600"
|
? "text-purple-600"
|
||||||
: "text-gray-600";
|
: "text-muted-foreground";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={group.id} className="rounded-lg border p-4">
|
<div key={group.id} className="rounded-lg border p-4">
|
||||||
|
|
|
||||||
|
|
@ -1592,11 +1592,11 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
<div className="bg-muted flex items-center justify-center gap-2 border-b p-4">
|
<div className="bg-muted flex items-center justify-center gap-2 border-b p-4">
|
||||||
<span className="text-muted-foreground text-sm font-medium">도구:</span>
|
<span className="text-muted-foreground text-sm font-medium">도구:</span>
|
||||||
{[
|
{[
|
||||||
{ type: "area" as ToolType, label: "영역", icon: Grid3x3, color: "text-blue-500" },
|
{ type: "area" as ToolType, label: "영역", icon: Grid3x3, color: "text-primary" },
|
||||||
{ type: "location-bed" as ToolType, label: "베드", icon: Package, color: "text-blue-600" },
|
{ type: "location-bed" as ToolType, label: "베드", icon: Package, color: "text-primary" },
|
||||||
{ type: "location-stp" as ToolType, label: "정차", icon: Move, color: "text-gray-500" },
|
{ type: "location-stp" as ToolType, label: "정차", icon: Move, color: "text-muted-foreground" },
|
||||||
// { type: "crane-gantry" as ToolType, label: "겐트리", icon: Combine, color: "text-green-500" },
|
// { type: "crane-gantry" as ToolType, label: "겐트리", icon: Combine, color: "text-emerald-500" },
|
||||||
{ type: "crane-mobile" as ToolType, label: "크레인", icon: Truck, color: "text-yellow-500" },
|
{ type: "crane-mobile" as ToolType, label: "크레인", icon: Truck, color: "text-amber-500" },
|
||||||
{ type: "rack" as ToolType, label: "랙", icon: Box, color: "text-purple-500" },
|
{ type: "rack" as ToolType, label: "랙", icon: Box, color: "text-purple-500" },
|
||||||
].map((tool) => {
|
].map((tool) => {
|
||||||
const Icon = tool.icon;
|
const Icon = tool.icon;
|
||||||
|
|
@ -1919,7 +1919,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isLocationPlaced ? (
|
{isLocationPlaced ? (
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
<Check className="h-4 w-4 text-emerald-500" />
|
||||||
) : locationType === "location-stp" ? (
|
) : locationType === "location-stp" ? (
|
||||||
<ParkingCircle className="text-muted-foreground h-4 w-4" />
|
<ParkingCircle className="text-muted-foreground h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -2052,7 +2052,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 중앙: 3D 캔버스 */}
|
{/* 중앙: 3D 캔버스 */}
|
||||||
<div className="relative h-full flex-1 bg-gray-100">
|
<div className="relative h-full flex-1 bg-muted">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
||||||
|
|
|
||||||
|
|
@ -716,7 +716,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{obj.materialCount !== undefined && obj.materialCount > 0 && (
|
{obj.materialCount !== undefined && obj.materialCount > 0 && (
|
||||||
<p className="text-xs text-yellow-600">
|
<p className="text-xs text-amber-600">
|
||||||
자재: <span className="font-semibold">{obj.materialCount}개</span>
|
자재: <span className="font-semibold">{obj.materialCount}개</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -802,7 +802,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{locationObj.materialCount !== undefined && locationObj.materialCount > 0 && (
|
{locationObj.materialCount !== undefined && locationObj.materialCount > 0 && (
|
||||||
<p className="mt-0.5 text-[10px] text-yellow-600">
|
<p className="mt-0.5 text-[10px] text-amber-600">
|
||||||
자재: <span className="font-semibold">{locationObj.materialCount}개</span>
|
자재: <span className="font-semibold">{locationObj.materialCount}개</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1239,7 +1239,7 @@ export default function Yard3DCanvas({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full bg-gray-100" onClick={handleCanvasClick}>
|
<div className="h-full w-full bg-muted" onClick={handleCanvasClick}>
|
||||||
<Canvas
|
<Canvas
|
||||||
camera={{
|
camera={{
|
||||||
position: [50, 30, 50],
|
position: [50, 30, 50],
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,16 @@ export const OBJECT_COLORS: Record<string, string> = {
|
||||||
|
|
||||||
// Tailwind 색상 클래스 매핑 (아이콘용)
|
// Tailwind 색상 클래스 매핑 (아이콘용)
|
||||||
export const OBJECT_COLOR_CLASSES: Record<string, string> = {
|
export const OBJECT_COLOR_CLASSES: Record<string, string> = {
|
||||||
area: "text-blue-500",
|
area: "text-primary",
|
||||||
"location-bed": "text-blue-600",
|
"location-bed": "text-primary",
|
||||||
"location-stp": "text-gray-500",
|
"location-stp": "text-muted-foreground",
|
||||||
"location-temp": "text-orange-500",
|
"location-temp": "text-amber-500",
|
||||||
"location-dest": "text-emerald-500",
|
"location-dest": "text-emerald-500",
|
||||||
"crane-mobile": "text-purple-500",
|
"crane-mobile": "text-purple-500",
|
||||||
rack: "text-red-500",
|
rack: "text-destructive",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 기본 색상
|
// 기본 색상
|
||||||
export const DEFAULT_COLOR = "#3b82f6";
|
export const DEFAULT_COLOR = "#3b82f6";
|
||||||
export const DEFAULT_COLOR_CLASS = "text-blue-500";
|
export const DEFAULT_COLOR_CLASS = "text-primary";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ function CategoryNode({
|
||||||
<Folder className="h-4 w-4 shrink-0 text-amber-500" />
|
<Folder className="h-4 w-4 shrink-0 text-amber-500" />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Tag className="h-4 w-4 shrink-0 text-blue-500" />
|
<Tag className="h-4 w-4 shrink-0 text-primary" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 카테고리 이름 */}
|
{/* 카테고리 이름 */}
|
||||||
|
|
|
||||||
|
|
@ -309,8 +309,8 @@ export function KeyGenerateModal({
|
||||||
preview?.exists
|
preview?.exists
|
||||||
? "border-destructive bg-destructive/10"
|
? "border-destructive bg-destructive/10"
|
||||||
: preview?.isOverride
|
: preview?.isOverride
|
||||||
? "border-blue-500 bg-blue-500/10"
|
? "border-primary bg-primary/10"
|
||||||
: "border-green-500 bg-green-500/10"
|
: "border-emerald-500 bg-emerald-500/10"
|
||||||
)}>
|
)}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{previewLoading ? (
|
{previewLoading ? (
|
||||||
|
|
@ -318,9 +318,9 @@ export function KeyGenerateModal({
|
||||||
) : preview?.exists ? (
|
) : preview?.exists ? (
|
||||||
<AlertCircle className="h-4 w-4 text-destructive" />
|
<AlertCircle className="h-4 w-4 text-destructive" />
|
||||||
) : preview?.isOverride ? (
|
) : preview?.isOverride ? (
|
||||||
<Info className="h-4 w-4 text-blue-500" />
|
<Info className="h-4 w-4 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
||||||
)}
|
)}
|
||||||
<code className="text-xs font-mono sm:text-sm">
|
<code className="text-xs font-mono sm:text-sm">
|
||||||
{generatedKeyPreview}
|
{generatedKeyPreview}
|
||||||
|
|
@ -332,7 +332,7 @@ export function KeyGenerateModal({
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{preview?.isOverride && !preview?.exists && (
|
{preview?.isOverride && !preview?.exists && (
|
||||||
<p className="mt-1 text-xs text-blue-600">
|
<p className="mt-1 text-xs text-primary">
|
||||||
공통 키가 존재합니다. 회사별 오버라이드로 생성됩니다.
|
공통 키가 존재합니다. 회사별 오버라이드로 생성됩니다.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,6 @@ export function ErrorMessage({ message }: ErrorMessageProps) {
|
||||||
if (!message) return null;
|
if (!message) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4 rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm text-red-700">{message}</div>
|
<div className="my-4 rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm text-destructive">{message}</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ function DraggableItem({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={drag}
|
ref={drag}
|
||||||
className={`flex cursor-move items-center gap-2 rounded border p-2 text-sm hover:border-blue-500 hover:bg-blue-50 ${
|
className={`flex cursor-move items-center gap-2 rounded border p-2 text-sm hover:border-primary hover:bg-primary/10 ${
|
||||||
isDragging ? "opacity-50" : ""
|
isDragging ? "opacity-50" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export function BarcodeDesignerCanvas() {
|
||||||
}), [widthPx, heightPx, components.length, addComponent, snapValueToGrid]);
|
}), [widthPx, heightPx, components.length, addComponent, snapValueToGrid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 items-center justify-center overflow-auto bg-gray-100 p-6">
|
<div className="flex flex-1 items-center justify-center overflow-auto bg-muted p-6">
|
||||||
<div
|
<div
|
||||||
key={`canvas-${widthMm}-${heightMm}`}
|
key={`canvas-${widthMm}-${heightMm}`}
|
||||||
ref={(r) => {
|
ref={(r) => {
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ export function BarcodeLabelCanvasComponent({ component }: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-gray-100 text-xs text-gray-400">
|
<div className="flex h-full w-full items-center justify-center bg-muted text-xs text-muted-foreground/70">
|
||||||
이미지
|
이미지
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -244,7 +244,7 @@ export function BarcodeLabelCanvasComponent({ component }: Props) {
|
||||||
{selected && component.type !== "line" && (
|
{selected && component.type !== "line" && (
|
||||||
<div
|
<div
|
||||||
data-resize-handle
|
data-resize-handle
|
||||||
className="absolute bottom-0 right-0 h-2 w-2 cursor-se-resize bg-blue-500"
|
className="absolute bottom-0 right-0 h-2 w-2 cursor-se-resize bg-primary"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsResizing(true);
|
setIsResizing(true);
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ export function BarcodePrintPreviewModal({
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* 미리보기 캔버스 (축소) */}
|
{/* 미리보기 캔버스 (축소) */}
|
||||||
<div className="flex justify-center rounded border bg-gray-100 p-4">
|
<div className="flex justify-center rounded border bg-muted p-4">
|
||||||
<div
|
<div
|
||||||
className="relative bg-white shadow"
|
className="relative bg-white shadow"
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -27,26 +27,26 @@ interface AlertModalProps {
|
||||||
const alertConfig = {
|
const alertConfig = {
|
||||||
success: {
|
success: {
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
iconColor: "text-green-500",
|
iconColor: "text-emerald-500",
|
||||||
titleColor: "text-green-700",
|
titleColor: "text-emerald-700",
|
||||||
buttonVariant: "default" as const,
|
buttonVariant: "default" as const,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
icon: XCircle,
|
icon: XCircle,
|
||||||
iconColor: "text-red-500",
|
iconColor: "text-destructive",
|
||||||
titleColor: "text-red-700",
|
titleColor: "text-destructive",
|
||||||
buttonVariant: "destructive" as const,
|
buttonVariant: "destructive" as const,
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
icon: AlertTriangle,
|
icon: AlertTriangle,
|
||||||
iconColor: "text-yellow-500",
|
iconColor: "text-amber-500",
|
||||||
titleColor: "text-yellow-700",
|
titleColor: "text-yellow-700",
|
||||||
buttonVariant: "default" as const,
|
buttonVariant: "default" as const,
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
icon: Info,
|
icon: Info,
|
||||||
iconColor: "text-blue-500",
|
iconColor: "text-primary",
|
||||||
titleColor: "text-blue-700",
|
titleColor: "text-primary",
|
||||||
buttonVariant: "default" as const,
|
buttonVariant: "default" as const,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1131,7 +1131,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
<FileSpreadsheet className="h-5 w-5" />
|
<FileSpreadsheet className="h-5 w-5" />
|
||||||
엑셀 데이터 업로드
|
엑셀 데이터 업로드
|
||||||
{isMasterDetail && (
|
{isMasterDetail && (
|
||||||
<span className="ml-2 rounded bg-blue-100 px-2 py-0.5 text-xs font-normal text-blue-700">
|
<span className="ml-2 rounded bg-primary/10 px-2 py-0.5 text-xs font-normal text-primary">
|
||||||
마스터-디테일
|
마스터-디테일
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1322,15 +1322,15 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
isDragOver
|
isDragOver
|
||||||
? "border-primary bg-primary/5"
|
? "border-primary bg-primary/5"
|
||||||
: file
|
: file
|
||||||
? "border-green-500 bg-green-50"
|
? "border-emerald-500 bg-emerald-50"
|
||||||
: "border-muted-foreground/25 hover:border-primary hover:bg-muted/50"
|
: "border-muted-foreground/25 hover:border-primary hover:bg-muted/50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{file ? (
|
{file ? (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<FileSpreadsheet className="h-8 w-8 text-green-600" />
|
<FileSpreadsheet className="h-8 w-8 text-emerald-600" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-green-700">{file.name}</p>
|
<p className="text-sm font-medium text-emerald-700">{file.name}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
클릭하여 다른 파일 선택
|
클릭하여 다른 파일 선택
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -1561,11 +1561,11 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
|
|
||||||
{/* 중복 체크 안내 */}
|
{/* 중복 체크 안내 */}
|
||||||
{duplicateCheckCount > 0 ? (
|
{duplicateCheckCount > 0 ? (
|
||||||
<div className="rounded-md border border-blue-200 bg-blue-50 p-3">
|
<div className="rounded-md border border-primary/20 bg-primary/10 p-3">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<Copy className="mt-0.5 h-4 w-4 text-blue-600" />
|
<Copy className="mt-0.5 h-4 w-4 text-primary" />
|
||||||
<div className="text-[10px] text-blue-700 sm:text-xs">
|
<div className="text-[10px] text-primary sm:text-xs">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
중복 키: {columnMappings
|
중복 키: {columnMappings
|
||||||
.filter((m) => m.checkDuplicate && m.systemColumn)
|
.filter((m) => m.checkDuplicate && m.systemColumn)
|
||||||
|
|
@ -1581,12 +1581,12 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<span className="text-[10px] text-blue-700 sm:text-xs">중복 시:</span>
|
<span className="text-[10px] text-primary sm:text-xs">중복 시:</span>
|
||||||
<Select
|
<Select
|
||||||
value={duplicateAction}
|
value={duplicateAction}
|
||||||
onValueChange={(value) => setDuplicateAction(value as "overwrite" | "skip")}
|
onValueChange={(value) => setDuplicateAction(value as "overwrite" | "skip")}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 w-[100px] text-[10px] sm:text-xs border-blue-300 bg-white">
|
<SelectTrigger className="h-7 w-[100px] text-[10px] sm:text-xs border-primary/40 bg-white">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -1686,7 +1686,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
<span className="font-medium">{mapping.excelColumn}</span> →{" "}
|
<span className="font-medium">{mapping.excelColumn}</span> →{" "}
|
||||||
{col?.label || mapping.systemColumn}
|
{col?.label || mapping.systemColumn}
|
||||||
{mapping.checkDuplicate && (
|
{mapping.checkDuplicate && (
|
||||||
<span className="ml-2 text-blue-600">
|
<span className="ml-2 text-primary">
|
||||||
(중복 체크: {mapping.duplicateAction === "overwrite" ? "덮어쓰기" : "건너뛰기"})
|
(중복 체크: {mapping.duplicateAction === "overwrite" ? "덮어쓰기" : "건너뛰기"})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1701,9 +1701,9 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
|
|
||||||
{/* 중복 체크 요약 */}
|
{/* 중복 체크 요약 */}
|
||||||
{duplicateCheckCount > 0 && (
|
{duplicateCheckCount > 0 && (
|
||||||
<div className="rounded-md border border-blue-200 bg-blue-50 p-4">
|
<div className="rounded-md border border-primary/20 bg-primary/10 p-4">
|
||||||
<h3 className="text-sm font-medium text-blue-800 sm:text-base">중복 체크 설정</h3>
|
<h3 className="text-sm font-medium text-primary sm:text-base">중복 체크 설정</h3>
|
||||||
<div className="mt-2 space-y-1 text-[10px] text-blue-700 sm:text-xs">
|
<div className="mt-2 space-y-1 text-[10px] text-primary sm:text-xs">
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">중복 키:</span>{" "}
|
<span className="font-medium">중복 키:</span>{" "}
|
||||||
{columnMappings
|
{columnMappings
|
||||||
|
|
|
||||||
|
|
@ -140,9 +140,9 @@ const CompactValidationIndicator: React.FC<{
|
||||||
<span className="text-destructive">{validationState.errors.length}개 오류</span>
|
<span className="text-destructive">{validationState.errors.length}개 오류</span>
|
||||||
)}
|
)}
|
||||||
{validationState.warnings.length > 0 && (
|
{validationState.warnings.length > 0 && (
|
||||||
<span className="ml-2 text-orange-600">{validationState.warnings.length}개 경고</span>
|
<span className="ml-2 text-amber-600">{validationState.warnings.length}개 경고</span>
|
||||||
)}
|
)}
|
||||||
{validationState.isValid && <span className="text-green-600">검증 통과</span>}
|
{validationState.isValid && <span className="text-emerald-600">검증 통과</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="sm" onClick={onSave} disabled={!canSave || saveState.status === "saving"} className="h-8">
|
<Button size="sm" onClick={onSave} disabled={!canSave || saveState.status === "saving"} className="h-8">
|
||||||
|
|
@ -176,7 +176,7 @@ const ValidationStatusBadge: React.FC<{ status: FormValidationState["status"] }>
|
||||||
variant: "default" as const,
|
variant: "default" as const,
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
text: "유효함",
|
text: "유효함",
|
||||||
className: "bg-green-500 hover:bg-green-600",
|
className: "bg-emerald-500 hover:bg-emerald-600",
|
||||||
};
|
};
|
||||||
case "invalid":
|
case "invalid":
|
||||||
return {
|
return {
|
||||||
|
|
@ -238,14 +238,14 @@ const ValidationSummary: React.FC<{ validationState: FormValidationState }> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{validationState.warnings.length > 0 && (
|
{validationState.warnings.length > 0 && (
|
||||||
<div className="flex items-center gap-1 text-orange-600">
|
<div className="flex items-center gap-1 text-amber-600">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<span>{validationState.warnings.length}개 경고</span>
|
<span>{validationState.warnings.length}개 경고</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{validationState.isValid && validationState.errors.length === 0 && validationState.warnings.length === 0 && (
|
{validationState.isValid && validationState.errors.length === 0 && validationState.warnings.length === 0 && (
|
||||||
<div className="flex items-center gap-1 text-green-600">
|
<div className="flex items-center gap-1 text-emerald-600">
|
||||||
<CheckCircle className="h-4 w-4" />
|
<CheckCircle className="h-4 w-4" />
|
||||||
<span>모든 검증 통과</span>
|
<span>모든 검증 통과</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -307,8 +307,8 @@ const ValidationErrorItem: React.FC<{ error: ValidationError }> = ({ error }) =>
|
||||||
*/
|
*/
|
||||||
const ValidationWarningItem: React.FC<{ warning: ValidationWarning }> = ({ warning }) => {
|
const ValidationWarningItem: React.FC<{ warning: ValidationWarning }> = ({ warning }) => {
|
||||||
return (
|
return (
|
||||||
<Alert className="border-orange-200 bg-orange-50 py-2">
|
<Alert className="border-orange-200 bg-amber-50 py-2">
|
||||||
<AlertTriangle className="h-4 w-4 text-orange-600" />
|
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||||
<AlertDescription className="text-sm">
|
<AlertDescription className="text-sm">
|
||||||
<span className="font-medium">{warning.field}:</span> {warning.message}
|
<span className="font-medium">{warning.field}:</span> {warning.message}
|
||||||
{warning.suggestion && <span className="mt-1 block text-xs text-orange-700">💡 {warning.suggestion}</span>}
|
{warning.suggestion && <span className="mt-1 block text-xs text-orange-700">💡 {warning.suggestion}</span>}
|
||||||
|
|
@ -364,15 +364,15 @@ export const FieldValidationIndicator: React.FC<{
|
||||||
{showIcon && (
|
{showIcon && (
|
||||||
<>
|
<>
|
||||||
{status === "validating" && <Clock className="text-muted-foreground h-3 w-3 animate-spin" />}
|
{status === "validating" && <Clock className="text-muted-foreground h-3 w-3 animate-spin" />}
|
||||||
{status === "valid" && !error && <CheckCircle className="h-3 w-3 text-green-600" />}
|
{status === "valid" && !error && <CheckCircle className="h-3 w-3 text-emerald-600" />}
|
||||||
{error && <AlertCircle className="text-destructive h-3 w-3" />}
|
{error && <AlertCircle className="text-destructive h-3 w-3" />}
|
||||||
{warning && !error && <AlertTriangle className="h-3 w-3 text-orange-600" />}
|
{warning && !error && <AlertTriangle className="h-3 w-3 text-amber-600" />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <span className="text-destructive">{error.message}</span>}
|
{error && <span className="text-destructive">{error.message}</span>}
|
||||||
|
|
||||||
{warning && !error && <span className="text-orange-600">{warning.message}</span>}
|
{warning && !error && <span className="text-amber-600">{warning.message}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1028,7 +1028,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-primary"></div>
|
||||||
<p className="text-muted-foreground">화면을 불러오는 중...</p>
|
<p className="text-muted-foreground">화면을 불러오는 중...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -102,26 +102,26 @@ export function TableHistoryModal({
|
||||||
const getOperationIcon = (type: string) => {
|
const getOperationIcon = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "INSERT":
|
case "INSERT":
|
||||||
return <Plus className="h-4 w-4 text-green-600" />;
|
return <Plus className="h-4 w-4 text-emerald-600" />;
|
||||||
case "UPDATE":
|
case "UPDATE":
|
||||||
return <FileEdit className="h-4 w-4 text-blue-600" />;
|
return <FileEdit className="h-4 w-4 text-primary" />;
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
return <Trash2 className="h-4 w-4 text-red-600" />;
|
return <Trash2 className="h-4 w-4 text-destructive" />;
|
||||||
default:
|
default:
|
||||||
return <Clock className="h-4 w-4 text-gray-600" />;
|
return <Clock className="h-4 w-4 text-muted-foreground" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOperationBadge = (type: string) => {
|
const getOperationBadge = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "INSERT":
|
case "INSERT":
|
||||||
return <span className="text-sm font-medium text-green-600">추가</span>;
|
return <span className="text-sm font-medium text-emerald-600">추가</span>;
|
||||||
case "UPDATE":
|
case "UPDATE":
|
||||||
return <span className="text-sm font-medium text-blue-600">수정</span>;
|
return <span className="text-sm font-medium text-primary">수정</span>;
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
return <span className="text-sm font-medium text-red-600">삭제</span>;
|
return <span className="text-sm font-medium text-destructive">삭제</span>;
|
||||||
default:
|
default:
|
||||||
return <span className="text-sm font-medium text-gray-600">{type}</span>;
|
return <span className="text-sm font-medium text-muted-foreground">{type}</span>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -301,8 +301,8 @@ export function TableHistoryModal({
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{timeline.map((event, index) => (
|
{timeline.map((event, index) => (
|
||||||
<div key={index} className="relative border-l-2 border-gray-200 pb-6 pl-8 last:border-l-0">
|
<div key={index} className="relative border-l-2 border-border pb-6 pl-8 last:border-l-0">
|
||||||
<div className="absolute top-0 -left-3 rounded-full border-2 border-gray-200 bg-white p-1">
|
<div className="absolute top-0 -left-3 rounded-full border-2 border-border bg-white p-1">
|
||||||
{getOperationIcon(event.operation_type)}
|
{getOperationIcon(event.operation_type)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -325,12 +325,12 @@ export function TableHistoryModal({
|
||||||
<p className="text-muted-foreground text-xs font-medium">변경된 항목:</p>
|
<p className="text-muted-foreground text-xs font-medium">변경된 항목:</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{event.changes.map((change, idx) => (
|
{event.changes.map((change, idx) => (
|
||||||
<div key={idx} className="rounded bg-gray-50 p-2 text-xs">
|
<div key={idx} className="rounded bg-muted p-2 text-xs">
|
||||||
<span className="font-mono font-medium">{change.column}</span>
|
<span className="font-mono font-medium">{change.column}</span>
|
||||||
<div className="mt-1 flex items-center gap-2">
|
<div className="mt-1 flex items-center gap-2">
|
||||||
<span className="text-red-600 line-through">{change.oldValue || "(없음)"}</span>
|
<span className="text-destructive line-through">{change.oldValue || "(없음)"}</span>
|
||||||
<span>→</span>
|
<span>→</span>
|
||||||
<span className="font-medium text-green-600">{change.newValue || "(없음)"}</span>
|
<span className="font-medium text-emerald-600">{change.newValue || "(없음)"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -355,7 +355,7 @@ export function TableHistoryModal({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead className="sticky top-0 border-b bg-gray-50">
|
<thead className="sticky top-0 border-b bg-muted">
|
||||||
<tr>
|
<tr>
|
||||||
{!recordId && <th className="p-2 text-left font-medium">레코드</th>}
|
{!recordId && <th className="p-2 text-left font-medium">레코드</th>}
|
||||||
<th className="p-2 text-left font-medium">작업</th>
|
<th className="p-2 text-left font-medium">작업</th>
|
||||||
|
|
@ -370,11 +370,11 @@ export function TableHistoryModal({
|
||||||
{detailRecords.map((record) => {
|
{detailRecords.map((record) => {
|
||||||
const displayValue = getDisplayValue(record);
|
const displayValue = getDisplayValue(record);
|
||||||
return (
|
return (
|
||||||
<tr key={record.log_id} className="border-b hover:bg-gray-50">
|
<tr key={record.log_id} className="border-b hover:bg-muted">
|
||||||
{!recordId && (
|
{!recordId && (
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
{displayValue ? (
|
{displayValue ? (
|
||||||
<span className="font-medium text-gray-900">{displayValue}</span>
|
<span className="font-medium text-foreground">{displayValue}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground text-xs">-</span>
|
<span className="text-muted-foreground text-xs">-</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -382,8 +382,8 @@ export function TableHistoryModal({
|
||||||
)}
|
)}
|
||||||
<td className="p-2">{getOperationBadge(record.operation_type)}</td>
|
<td className="p-2">{getOperationBadge(record.operation_type)}</td>
|
||||||
<td className="p-2 font-mono">{record.changed_column}</td>
|
<td className="p-2 font-mono">{record.changed_column}</td>
|
||||||
<td className="max-w-[200px] truncate p-2 text-red-600">{record.old_value || "-"}</td>
|
<td className="max-w-[200px] truncate p-2 text-destructive">{record.old_value || "-"}</td>
|
||||||
<td className="max-w-[200px] truncate p-2 text-green-600">{record.new_value || "-"}</td>
|
<td className="max-w-[200px] truncate p-2 text-emerald-600">{record.new_value || "-"}</td>
|
||||||
<td className="p-2">{record.changed_by}</td>
|
<td className="p-2">{record.changed_by}</td>
|
||||||
<td className="text-muted-foreground p-2">{formatDate(record.changed_at)}</td>
|
<td className="text-muted-foreground p-2">{formatDate(record.changed_at)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ interface ValidationMessageProps {
|
||||||
|
|
||||||
export function ValidationMessage({ message, isValid, isLoading, className }: ValidationMessageProps) {
|
export function ValidationMessage({ message, isValid, isLoading, className }: ValidationMessageProps) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <p className={cn("text-sm text-gray-500", className)}>검사 중...</p>;
|
return <p className={cn("text-sm text-muted-foreground", className)}>검사 중...</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
|
|
@ -18,6 +18,6 @@ export function ValidationMessage({ message, isValid, isLoading, className }: Va
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className={cn("text-sm transition-colors", isValid ? "text-green-600" : "text-destructive", className)}>{message}</p>
|
<p className={cn("text-sm transition-colors", isValid ? "text-emerald-600" : "text-destructive", className)}>{message}</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,16 +212,16 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
||||||
const renderIcon = (icon?: string, color?: string) => {
|
const renderIcon = (icon?: string, color?: string) => {
|
||||||
const colorClass =
|
const colorClass =
|
||||||
color === "blue"
|
color === "blue"
|
||||||
? "text-blue-600"
|
? "text-primary"
|
||||||
: color === "orange"
|
: color === "orange"
|
||||||
? "text-orange-600"
|
? "text-amber-600"
|
||||||
: color === "green"
|
: color === "green"
|
||||||
? "text-green-600"
|
? "text-emerald-600"
|
||||||
: color === "red"
|
: color === "red"
|
||||||
? "text-red-600"
|
? "text-destructive"
|
||||||
: color === "purple"
|
: color === "purple"
|
||||||
? "text-purple-600"
|
? "text-purple-600"
|
||||||
: "text-gray-600";
|
: "text-muted-foreground";
|
||||||
|
|
||||||
switch (icon) {
|
switch (icon) {
|
||||||
case "truck":
|
case "truck":
|
||||||
|
|
@ -241,16 +241,16 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
||||||
const renderFieldGroup = (group: FieldGroup, groupData: Record<string, any>) => {
|
const renderFieldGroup = (group: FieldGroup, groupData: Record<string, any>) => {
|
||||||
const colorClass =
|
const colorClass =
|
||||||
group.color === "blue"
|
group.color === "blue"
|
||||||
? "text-blue-600"
|
? "text-primary"
|
||||||
: group.color === "orange"
|
: group.color === "orange"
|
||||||
? "text-orange-600"
|
? "text-amber-600"
|
||||||
: group.color === "green"
|
: group.color === "green"
|
||||||
? "text-green-600"
|
? "text-emerald-600"
|
||||||
: group.color === "red"
|
: group.color === "red"
|
||||||
? "text-red-600"
|
? "text-destructive"
|
||||||
: group.color === "purple"
|
: group.color === "purple"
|
||||||
? "text-purple-600"
|
? "text-purple-600"
|
||||||
: "text-gray-600";
|
: "text-muted-foreground";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={group.id} className="rounded-lg border p-4">
|
<div key={group.id} className="rounded-lg border p-4">
|
||||||
|
|
|
||||||
|
|
@ -1352,8 +1352,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
|
|
||||||
{/* 이동경로 날짜 선택 */}
|
{/* 이동경로 날짜 선택 */}
|
||||||
{selectedUserId && (
|
{selectedUserId && (
|
||||||
<div className="flex items-center gap-1 rounded border bg-blue-50 px-2 py-1">
|
<div className="flex items-center gap-1 rounded border bg-primary/10 px-2 py-1">
|
||||||
<span className="text-xs text-blue-600">🛣️</span>
|
<span className="text-xs text-primary">🛣️</span>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={routeDate}
|
value={routeDate}
|
||||||
|
|
@ -1363,10 +1363,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
loadRoute(selectedUserId, e.target.value);
|
loadRoute(selectedUserId, e.target.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="h-6 rounded border-none bg-transparent px-1 text-xs text-blue-600 focus:outline-none"
|
className="h-6 rounded border-none bg-transparent px-1 text-xs text-primary focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-blue-600">({routePoints.length}개)</span>
|
<span className="text-xs text-primary">({routePoints.length}개)</span>
|
||||||
<button onClick={clearRoute} className="ml-1 text-xs text-blue-400 hover:text-blue-600">
|
<button onClick={clearRoute} className="ml-1 text-xs text-primary/80 hover:text-primary">
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1494,15 +1494,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
const parsed = JSON.parse(matchingPolygon.description);
|
const parsed = JSON.parse(matchingPolygon.description);
|
||||||
popupContent = `
|
popupContent = `
|
||||||
<div class="min-w-[200px]">
|
<div class="min-w-[200px]">
|
||||||
${matchingPolygon.source ? `<div class="mb-2 border-b pb-2"><div class="text-gray-500 text-xs">📡 ${matchingPolygon.source}</div></div>` : ""}
|
${matchingPolygon.source ? `<div class="mb-2 border-b pb-2"><div class="text-muted-foreground text-xs">📡 ${matchingPolygon.source}</div></div>` : ""}
|
||||||
<div class="bg-gray-100 rounded p-2">
|
<div class="bg-muted rounded p-2">
|
||||||
<div class="text-gray-900 mb-1 text-xs font-semibold">상세 정보</div>
|
<div class="text-foreground mb-1 text-xs font-semibold">상세 정보</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
${popupFields
|
${popupFields
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
const value = parsed[field.fieldName];
|
const value = parsed[field.fieldName];
|
||||||
if (value === undefined || value === null) return "";
|
if (value === undefined || value === null) return "";
|
||||||
return `<div class="text-xs"><span class="text-gray-600 font-medium">${field.label}:</span> <span class="text-gray-900">${value}</span></div>`;
|
return `<div class="text-xs"><span class="text-muted-foreground font-medium">${field.label}:</span> <span class="text-foreground">${value}</span></div>`;
|
||||||
})
|
})
|
||||||
.join("")}
|
.join("")}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1980,7 +1980,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
<button
|
<button
|
||||||
onClick={() => loadTripInfo(identifier)}
|
onClick={() => loadTripInfo(identifier)}
|
||||||
disabled={tripInfoLoading === identifier}
|
disabled={tripInfoLoading === identifier}
|
||||||
className="w-full rounded bg-gray-100 px-2 py-1.5 text-xs text-gray-700 hover:bg-gray-200 disabled:opacity-50"
|
className="w-full rounded bg-muted px-2 py-1.5 text-xs text-foreground hover:bg-muted/80 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{tripInfoLoading === identifier ? "로딩 중..." : "📊 운행/공차 정보 보기"}
|
{tripInfoLoading === identifier ? "로딩 중..." : "📊 운행/공차 정보 보기"}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1996,10 +1996,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
{/* 운행 정보 */}
|
{/* 운행 정보 */}
|
||||||
{hasTripInfo && (
|
{hasTripInfo && (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="text-xs font-semibold text-blue-600 mb-1">🚛 최근 운행</div>
|
<div className="text-xs font-semibold text-primary mb-1">🚛 최근 운행</div>
|
||||||
<div className="bg-blue-50 rounded p-2 space-y-1">
|
<div className="bg-primary/10 rounded p-2 space-y-1">
|
||||||
{(info.last_trip_start || info.last_trip_end) && (
|
{(info.last_trip_start || info.last_trip_end) && (
|
||||||
<div className="text-[10px] text-gray-600">
|
<div className="text-[10px] text-muted-foreground">
|
||||||
<span className="font-medium">시간:</span>{" "}
|
<span className="font-medium">시간:</span>{" "}
|
||||||
{formatDateTime(info.last_trip_start)} ~ {formatDateTime(info.last_trip_end)}
|
{formatDateTime(info.last_trip_start)} ~ {formatDateTime(info.last_trip_end)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2007,20 +2007,20 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
<div className="flex gap-3 text-[10px]">
|
<div className="flex gap-3 text-[10px]">
|
||||||
{info.last_trip_distance !== undefined && info.last_trip_distance !== null && (
|
{info.last_trip_distance !== undefined && info.last_trip_distance !== null && (
|
||||||
<span>
|
<span>
|
||||||
<span className="font-medium text-gray-600">거리:</span>{" "}
|
<span className="font-medium text-muted-foreground">거리:</span>{" "}
|
||||||
<span className="text-blue-700 font-semibold">{formatDistance(info.last_trip_distance)}</span>
|
<span className="text-primary font-semibold">{formatDistance(info.last_trip_distance)}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{info.last_trip_time !== undefined && info.last_trip_time !== null && (
|
{info.last_trip_time !== undefined && info.last_trip_time !== null && (
|
||||||
<span>
|
<span>
|
||||||
<span className="font-medium text-gray-600">소요:</span>{" "}
|
<span className="font-medium text-muted-foreground">소요:</span>{" "}
|
||||||
<span className="text-blue-700 font-semibold">{formatTime(info.last_trip_time)}</span>
|
<span className="text-primary font-semibold">{formatTime(info.last_trip_time)}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* 출발지/도착지 */}
|
{/* 출발지/도착지 */}
|
||||||
{(info.departure || info.arrival) && (
|
{(info.departure || info.arrival) && (
|
||||||
<div className="text-[10px] text-gray-600 pt-1 border-t border-blue-100">
|
<div className="text-[10px] text-muted-foreground pt-1 border-t border-primary/10">
|
||||||
{info.departure && <span>출발: {info.departure}</span>}
|
{info.departure && <span>출발: {info.departure}</span>}
|
||||||
{info.departure && info.arrival && " → "}
|
{info.departure && info.arrival && " → "}
|
||||||
{info.arrival && <span>도착: {info.arrival}</span>}
|
{info.arrival && <span>도착: {info.arrival}</span>}
|
||||||
|
|
@ -2033,10 +2033,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
{/* 공차 정보 */}
|
{/* 공차 정보 */}
|
||||||
{hasEmptyTripInfo && (
|
{hasEmptyTripInfo && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-semibold text-orange-600 mb-1">📦 최근 공차</div>
|
<div className="text-xs font-semibold text-amber-600 mb-1">📦 최근 공차</div>
|
||||||
<div className="bg-orange-50 rounded p-2 space-y-1">
|
<div className="bg-amber-50 rounded p-2 space-y-1">
|
||||||
{(info.last_empty_start || info.last_empty_end) && (
|
{(info.last_empty_start || info.last_empty_end) && (
|
||||||
<div className="text-[10px] text-gray-600">
|
<div className="text-[10px] text-muted-foreground">
|
||||||
<span className="font-medium">시간:</span>{" "}
|
<span className="font-medium">시간:</span>{" "}
|
||||||
{formatDateTime(info.last_empty_start)} ~ {formatDateTime(info.last_empty_end)}
|
{formatDateTime(info.last_empty_start)} ~ {formatDateTime(info.last_empty_end)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2044,13 +2044,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
<div className="flex gap-3 text-[10px]">
|
<div className="flex gap-3 text-[10px]">
|
||||||
{info.last_empty_distance !== undefined && info.last_empty_distance !== null && (
|
{info.last_empty_distance !== undefined && info.last_empty_distance !== null && (
|
||||||
<span>
|
<span>
|
||||||
<span className="font-medium text-gray-600">거리:</span>{" "}
|
<span className="font-medium text-muted-foreground">거리:</span>{" "}
|
||||||
<span className="text-orange-700 font-semibold">{formatDistance(info.last_empty_distance)}</span>
|
<span className="text-orange-700 font-semibold">{formatDistance(info.last_empty_distance)}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{info.last_empty_time !== undefined && info.last_empty_time !== null && (
|
{info.last_empty_time !== undefined && info.last_empty_time !== null && (
|
||||||
<span>
|
<span>
|
||||||
<span className="font-medium text-gray-600">소요:</span>{" "}
|
<span className="font-medium text-muted-foreground">소요:</span>{" "}
|
||||||
<span className="text-orange-700 font-semibold">{formatTime(info.last_empty_time)}</span>
|
<span className="text-orange-700 font-semibold">{formatTime(info.last_empty_time)}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -2085,7 +2085,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
<button
|
<button
|
||||||
onClick={() => loadRoute(visibleUserId)}
|
onClick={() => loadRoute(visibleUserId)}
|
||||||
disabled={routeLoading}
|
disabled={routeLoading}
|
||||||
className="w-full rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600 disabled:opacity-50"
|
className="w-full rounded bg-primary px-2 py-1 text-xs text-white hover:bg-primary disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{routeLoading && selectedUserId === visibleUserId ? "로딩 중..." : "🛣️ 이동경로 보기"}
|
{routeLoading && selectedUserId === visibleUserId ? "로딩 중..." : "🛣️ 이동경로 보기"}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -357,7 +357,7 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
||||||
<button
|
<button
|
||||||
onClick={() => loadRoute(vehicle)}
|
onClick={() => loadRoute(vehicle)}
|
||||||
disabled={isRouteLoading}
|
disabled={isRouteLoading}
|
||||||
className="w-full rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600 disabled:opacity-50"
|
className="w-full rounded bg-primary px-2 py-1 text-xs text-white hover:bg-primary disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isRouteLoading && selectedVehicle?.id === vehicle.id
|
{isRouteLoading && selectedVehicle?.id === vehicle.id
|
||||||
? "로딩 중..."
|
? "로딩 중..."
|
||||||
|
|
@ -391,7 +391,7 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
||||||
|
|
||||||
{/* 이동경로 정보 표시 - 상단으로 이동하여 주석 처리 */}
|
{/* 이동경로 정보 표시 - 상단으로 이동하여 주석 처리 */}
|
||||||
{/* {selectedVehicle && routePoints.length > 0 && (
|
{/* {selectedVehicle && routePoints.length > 0 && (
|
||||||
<div className="absolute bottom-2 right-2 z-[1000] rounded-lg bg-blue-500/90 p-2 shadow-lg backdrop-blur-sm">
|
<div className="absolute bottom-2 right-2 z-[1000] rounded-lg bg-primary/90 p-2 shadow-lg backdrop-blur-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-xs text-white">
|
<div className="text-xs text-white">
|
||||||
<div className="font-semibold">🛣️ {selectedVehicle.name} 이동경로</div>
|
<div className="font-semibold">🛣️ {selectedVehicle.name} 이동경로</div>
|
||||||
|
|
|
||||||
|
|
@ -733,13 +733,13 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
<CheckCircle className="h-5 w-5 text-emerald-500" />
|
||||||
연결 생성 완료
|
연결 생성 완료
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription className="text-base">
|
<AlertDialogDescription className="text-base">
|
||||||
<span className="font-medium text-green-600">{createdConnectionName}</span> 연결이 생성되었습니다.
|
<span className="font-medium text-emerald-600">{createdConnectionName}</span> 연결이 생성되었습니다.
|
||||||
<br />
|
<br />
|
||||||
<span className="mt-2 block text-sm text-gray-500">
|
<span className="mt-2 block text-sm text-muted-foreground">
|
||||||
생성된 연결은 데이터플로우 다이어그램에서 확인할 수 있습니다.
|
생성된 연결은 데이터플로우 다이어그램에서 확인할 수 있습니다.
|
||||||
</span>
|
</span>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|
|
||||||
|
|
@ -800,7 +800,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="data-flow-designer h-screen bg-gray-100">
|
<div className="data-flow-designer h-screen bg-muted">
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
{/* 사이드바 */}
|
{/* 사이드바 */}
|
||||||
<DataFlowSidebar
|
<DataFlowSidebar
|
||||||
|
|
@ -892,12 +892,12 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
{/* 안내 메시지 */}
|
{/* 안내 메시지 */}
|
||||||
{nodes.length === 0 && (
|
{nodes.length === 0 && (
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
<div className="text-center text-gray-500">
|
<div className="text-center text-muted-foreground">
|
||||||
<div className="mb-2 text-2xl">📊</div>
|
<div className="mb-2 text-2xl">📊</div>
|
||||||
<div className="mb-1 text-lg font-medium">테이블 간 데이터 관계 설정을 시작하세요</div>
|
<div className="mb-1 text-lg font-medium">테이블 간 데이터 관계 설정을 시작하세요</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<div>왼쪽 사이드바에서 테이블을 더블클릭하여 추가하세요</div>
|
<div>왼쪽 사이드바에서 테이블을 더블클릭하여 추가하세요</div>
|
||||||
<div className="mt-1 text-xs text-gray-400">테이블 선택 후 Del 키로 삭제 가능</div>
|
<div className="mt-1 text-xs text-muted-foreground/70">테이블 선택 후 Del 키로 삭제 가능</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
||||||
getSelectedTableNames,
|
getSelectedTableNames,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-80 border-r border-gray-200 bg-white shadow-lg">
|
<div className="w-80 border-r border-border bg-white shadow-lg">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h2 className="mb-6 text-xl font-bold text-gray-800">테이블 간 데이터 관계 설정</h2>
|
<h2 className="mb-6 text-xl font-bold text-foreground">테이블 간 데이터 관계 설정</h2>
|
||||||
|
|
||||||
{/* 테이블 선택기 */}
|
{/* 테이블 선택기 */}
|
||||||
<TableSelector companyCode={companyCode} onTableAdd={onTableAdd} selectedTables={getSelectedTableNames()} />
|
<TableSelector companyCode={companyCode} onTableAdd={onTableAdd} selectedTables={getSelectedTableNames()} />
|
||||||
|
|
@ -46,7 +46,7 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<button
|
<button
|
||||||
onClick={onRemoveOrphanedNodes}
|
onClick={onRemoveOrphanedNodes}
|
||||||
className="w-full rounded-lg bg-orange-500 p-3 font-medium text-white transition-colors hover:bg-orange-600"
|
className="w-full rounded-lg bg-amber-500 p-3 font-medium text-white transition-colors hover:bg-orange-600"
|
||||||
disabled={nodes.length === 0}
|
disabled={nodes.length === 0}
|
||||||
>
|
>
|
||||||
🧹 고립된 노드 정리
|
🧹 고립된 노드 정리
|
||||||
|
|
@ -54,14 +54,14 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onClearAll}
|
onClick={onClearAll}
|
||||||
className="w-full rounded-lg bg-destructive/100 p-3 font-medium text-white transition-colors hover:bg-red-600"
|
className="w-full rounded-lg bg-destructive/100 p-3 font-medium text-white transition-colors hover:bg-destructive"
|
||||||
>
|
>
|
||||||
🗑️ 전체 삭제
|
🗑️ 전체 삭제
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onOpenSaveModal}
|
onClick={onOpenSaveModal}
|
||||||
className={`w-full rounded-lg bg-green-500 p-3 font-medium text-white transition-colors hover:bg-green-600 ${
|
className={`w-full rounded-lg bg-emerald-500 p-3 font-medium text-white transition-colors hover:bg-emerald-600 ${
|
||||||
hasUnsavedChanges ? "animate-pulse" : ""
|
hasUnsavedChanges ? "animate-pulse" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -70,8 +70,8 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 통계 정보 */}
|
{/* 통계 정보 */}
|
||||||
<div className="mt-6 rounded-lg bg-gray-50 p-4">
|
<div className="mt-6 rounded-lg bg-muted p-4">
|
||||||
<div className="mb-2 text-sm font-semibold text-gray-700">통계</div>
|
<div className="mb-2 text-sm font-semibold text-foreground">통계</div>
|
||||||
<div className="space-y-1 text-sm text-muted-foreground">
|
<div className="space-y-1 text-sm text-muted-foreground">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>테이블 노드:</span>
|
<span>테이블 노드:</span>
|
||||||
|
|
@ -83,7 +83,7 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>메모리 관계:</span>
|
<span>메모리 관계:</span>
|
||||||
<span className="font-medium text-orange-600">{tempRelationships.length}개</span>
|
<span className="font-medium text-amber-600">{tempRelationships.length}개</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>관계도 ID:</span>
|
<span>관계도 ID:</span>
|
||||||
|
|
@ -98,7 +98,7 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{hasUnsavedChanges && (
|
{hasUnsavedChanges && (
|
||||||
<div className="mt-2 text-xs font-medium text-orange-600">⚠️ 저장되지 않은 변경사항이 있습니다</div>
|
<div className="mt-2 text-xs font-medium text-amber-600">⚠️ 저장되지 않은 변경사항이 있습니다</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed z-50 rounded-xl border border-gray-200 bg-white shadow-2xl"
|
className="fixed z-50 rounded-xl border border-border bg-white shadow-2xl"
|
||||||
style={{
|
style={{
|
||||||
left: position.x - 160,
|
left: position.x - 160,
|
||||||
top: position.y - 100,
|
top: position.y - 100,
|
||||||
|
|
@ -33,7 +33,7 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between rounded-t-xl border-b border-gray-200 bg-blue-600 p-4">
|
<div className="flex items-center justify-between rounded-t-xl border-b border-border bg-primary p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
|
||||||
<span className="text-sm text-white">🔗</span>
|
<span className="text-sm text-white">🔗</span>
|
||||||
|
|
@ -52,10 +52,10 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 관계 정보 요약 */}
|
{/* 관계 정보 요약 */}
|
||||||
<div className="border-b border-gray-100 bg-gray-50 p-3">
|
<div className="border-b border-border bg-muted p-3">
|
||||||
<div className="flex items-center justify-center gap-4">
|
<div className="flex items-center justify-center gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-xs font-medium tracking-wide text-gray-500 uppercase">연결 유형</div>
|
<div className="text-xs font-medium tracking-wide text-muted-foreground uppercase">연결 유형</div>
|
||||||
<div className="mt-1 inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-semibold text-indigo-800">
|
<div className="mt-1 inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-semibold text-indigo-800">
|
||||||
{edgeInfo.connectionType}
|
{edgeInfo.connectionType}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -68,7 +68,7 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
||||||
{/* From 테이블 */}
|
{/* From 테이블 */}
|
||||||
<div className="rounded-lg border-l-4 border-emerald-400 bg-emerald-50 p-3">
|
<div className="rounded-lg border-l-4 border-emerald-400 bg-emerald-50 p-3">
|
||||||
<div className="mb-2 text-xs font-bold tracking-wide text-emerald-700 uppercase">FROM</div>
|
<div className="mb-2 text-xs font-bold tracking-wide text-emerald-700 uppercase">FROM</div>
|
||||||
<div className="mb-2 text-base font-bold text-gray-800">{edgeInfo.fromTable}</div>
|
<div className="mb-2 text-base font-bold text-foreground">{edgeInfo.fromTable}</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{edgeInfo.fromColumns.map((column, index) => (
|
{edgeInfo.fromColumns.map((column, index) => (
|
||||||
|
|
@ -89,15 +89,15 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* To 테이블 */}
|
{/* To 테이블 */}
|
||||||
<div className="rounded-lg border-l-4 border-blue-400 bg-accent p-3">
|
<div className="rounded-lg border-l-4 border-primary/60 bg-accent p-3">
|
||||||
<div className="mb-2 text-xs font-bold tracking-wide text-blue-700 uppercase">TO</div>
|
<div className="mb-2 text-xs font-bold tracking-wide text-primary uppercase">TO</div>
|
||||||
<div className="mb-2 text-base font-bold text-gray-800">{edgeInfo.toTable}</div>
|
<div className="mb-2 text-base font-bold text-foreground">{edgeInfo.toTable}</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{edgeInfo.toColumns.map((column, index) => (
|
{edgeInfo.toColumns.map((column, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className="inline-flex items-center rounded-md bg-primary/20 px-2.5 py-0.5 text-xs font-medium text-blue-800 ring-1 ring-blue-200"
|
className="inline-flex items-center rounded-md bg-primary/20 px-2.5 py-0.5 text-xs font-medium text-primary ring-1 ring-primary/20"
|
||||||
>
|
>
|
||||||
{column}
|
{column}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -108,16 +108,16 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 액션 버튼 */}
|
{/* 액션 버튼 */}
|
||||||
<div className="flex gap-2 border-t border-gray-200 bg-gray-50 p-3">
|
<div className="flex gap-2 border-t border-border bg-muted p-3">
|
||||||
<button
|
<button
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-blue-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-all hover:bg-blue-700 hover:shadow-md"
|
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-primary px-3 py-2 text-xs font-semibold text-white shadow-sm transition-all hover:bg-primary/90 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<span>수정</span>
|
<span>수정</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-red-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-all hover:bg-red-700 hover:shadow-md"
|
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-destructive px-3 py-2 text-xs font-semibold text-white shadow-sm transition-all hover:bg-red-700 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<span>삭제</span>
|
<span>삭제</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -136,16 +136,16 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-auto absolute top-4 right-4 z-40 w-80 rounded-xl border border-primary/20 bg-white shadow-lg">
|
<div className="pointer-events-auto absolute top-4 right-4 z-40 w-80 rounded-xl border border-primary/20 bg-white shadow-lg">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
|
<div className="flex items-center justify-between rounded-t-xl border-b border-primary/10 bg-gradient-to-r from-primary/5 to-indigo-50 p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="rounded-full bg-primary/20 p-1">
|
<div className="rounded-full bg-primary/20 p-1">
|
||||||
<span className="text-sm text-primary">🔗</span>
|
<span className="text-sm text-primary">🔗</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold text-gray-800">테이블 간 관계 목록</div>
|
<div className="text-sm font-semibold text-foreground">테이블 간 관계 목록</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-full text-gray-400 transition-colors hover:bg-gray-100 hover:text-muted-foreground"
|
className="flex h-6 w-6 items-center justify-center rounded-full text-muted-foreground/70 transition-colors hover:bg-muted hover:text-muted-foreground"
|
||||||
>
|
>
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
|
@ -159,10 +159,10 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
||||||
{relationships.map((relationship) => (
|
{relationships.map((relationship) => (
|
||||||
<div
|
<div
|
||||||
key={relationship.id}
|
key={relationship.id}
|
||||||
className="rounded-lg border border-gray-200 p-3 transition-all hover:border-blue-300 hover:bg-accent"
|
className="rounded-lg border border-border p-3 transition-all hover:border-primary/40 hover:bg-accent"
|
||||||
>
|
>
|
||||||
<div className="mb-1 flex items-center justify-between">
|
<div className="mb-1 flex items-center justify-between">
|
||||||
<h4 className="text-sm font-medium text-gray-900">
|
<h4 className="text-sm font-medium text-foreground">
|
||||||
{relationship.relationshipName || `${relationship.fromTable} → ${relationship.toTable}`}
|
{relationship.relationshipName || `${relationship.fromTable} → ${relationship.toTable}`}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|
@ -172,7 +172,7 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleEdit(relationship);
|
handleEdit(relationship);
|
||||||
}}
|
}}
|
||||||
className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-primary/20 hover:text-primary"
|
className="flex h-6 w-6 items-center justify-center rounded text-muted-foreground/70 hover:bg-primary/20 hover:text-primary"
|
||||||
title="관계 편집"
|
title="관계 편집"
|
||||||
>
|
>
|
||||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
|
@ -190,7 +190,7 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDelete(relationship);
|
handleDelete(relationship);
|
||||||
}}
|
}}
|
||||||
className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-destructive/20 hover:text-destructive"
|
className="flex h-6 w-6 items-center justify-center rounded text-muted-foreground/70 hover:bg-destructive/20 hover:text-destructive"
|
||||||
title="관계 삭제"
|
title="관계 삭제"
|
||||||
>
|
>
|
||||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
|
|
||||||
|
|
@ -161,13 +161,13 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 관계 요약 정보 */}
|
{/* 관계 요약 정보 */}
|
||||||
<div className="grid grid-cols-3 gap-4 rounded-lg bg-gray-50 p-4">
|
<div className="grid grid-cols-3 gap-4 rounded-lg bg-muted p-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-primary">{relationships.length}</div>
|
<div className="text-2xl font-bold text-primary">{relationships.length}</div>
|
||||||
<div className="text-sm text-muted-foreground">관계 수</div>
|
<div className="text-sm text-muted-foreground">관계 수</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-green-600">{connectedTables.length}</div>
|
<div className="text-2xl font-bold text-emerald-600">{connectedTables.length}</div>
|
||||||
<div className="text-sm text-muted-foreground">연결된 테이블</div>
|
<div className="text-sm text-muted-foreground">연결된 테이블</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|
@ -207,7 +207,7 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
{relationships.map((relationship, index) => (
|
{relationships.map((relationship, index) => (
|
||||||
<div
|
<div
|
||||||
key={relationship.id || index}
|
key={relationship.id || index}
|
||||||
className="flex items-center justify-between rounded-lg border bg-white p-3 hover:bg-gray-50"
|
className="flex items-center justify-between rounded-lg border bg-white p-3 hover:bg-muted"
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
|
@ -234,10 +234,10 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
|
|
||||||
{/* 관계가 없는 경우 안내 */}
|
{/* 관계가 없는 경우 안내 */}
|
||||||
{relationships.length === 0 && (
|
{relationships.length === 0 && (
|
||||||
<div className="py-8 text-center text-gray-500">
|
<div className="py-8 text-center text-muted-foreground">
|
||||||
<div className="mb-2 text-4xl">📭</div>
|
<div className="mb-2 text-4xl">📭</div>
|
||||||
<div className="text-sm">생성된 관계가 없습니다.</div>
|
<div className="text-sm">생성된 관계가 없습니다.</div>
|
||||||
<div className="mt-1 text-xs text-gray-400">테이블을 추가하고 컬럼을 연결해서 관계를 생성해보세요.</div>
|
<div className="mt-1 text-xs text-muted-foreground/70">테이블을 추가하고 컬럼을 연결해서 관계를 생성해보세요.</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -249,7 +249,7 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isLoading || relationships.length === 0}
|
disabled={isLoading || relationships.length === 0}
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-primary hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -269,13 +269,13 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
<CheckCircle className="h-5 w-5 text-emerald-500" />
|
||||||
관계도 저장 완료
|
관계도 저장 완료
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription className="text-base">
|
<AlertDialogDescription className="text-base">
|
||||||
<span className="font-medium text-green-600">{savedDiagramName}</span> 관계도가 성공적으로 저장되었습니다.
|
<span className="font-medium text-emerald-600">{savedDiagramName}</span> 관계도가 성공적으로 저장되었습니다.
|
||||||
<br />
|
<br />
|
||||||
<span className="mt-2 block text-sm text-gray-500">
|
<span className="mt-2 block text-sm text-muted-foreground">
|
||||||
저장된 관계도는 관리 메뉴에서 확인하고 수정할 수 있습니다.
|
저장된 관계도는 관리 메뉴에서 확인하고 수정할 수 있습니다.
|
||||||
</span>
|
</span>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,14 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-primary/20 bg-white shadow-lg">
|
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-primary/20 bg-white shadow-lg">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
|
<div className="flex items-center justify-between rounded-t-xl border-b border-primary/10 bg-gradient-to-r from-primary/5 to-indigo-50 p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/20">
|
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/20">
|
||||||
<span className="text-sm text-primary">📋</span>
|
<span className="text-sm text-primary">📋</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold text-gray-800">선택된 테이블</div>
|
<div className="text-sm font-semibold text-foreground">선택된 테이블</div>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-muted-foreground">
|
||||||
{selectedNodes.length === 1
|
{selectedNodes.length === 1
|
||||||
? "FROM 테이블 선택됨"
|
? "FROM 테이블 선택됨"
|
||||||
: selectedNodes.length === 2
|
: selectedNodes.length === 2
|
||||||
|
|
@ -44,7 +44,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-muted-foreground"
|
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground/70 hover:bg-muted hover:text-muted-foreground"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -66,14 +66,14 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
index === 0
|
index === 0
|
||||||
? "border-l-4 border-emerald-400 bg-emerald-50"
|
? "border-l-4 border-emerald-400 bg-emerald-50"
|
||||||
: index === 1
|
: index === 1
|
||||||
? "border-l-4 border-blue-400 bg-accent"
|
? "border-l-4 border-primary/60 bg-accent"
|
||||||
: "bg-muted"
|
: "bg-muted"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mb-1 flex items-center justify-between">
|
<div className="mb-1 flex items-center justify-between">
|
||||||
<div
|
<div
|
||||||
className={`text-xs font-medium ${
|
className={`text-xs font-medium ${
|
||||||
index === 0 ? "text-emerald-700" : index === 1 ? "text-blue-700" : "text-gray-700"
|
index === 0 ? "text-emerald-700" : index === 1 ? "text-primary" : "text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{displayName}
|
{displayName}
|
||||||
|
|
@ -81,7 +81,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
{selectedNodes.length === 2 && (
|
{selectedNodes.length === 2 && (
|
||||||
<div
|
<div
|
||||||
className={`rounded-full px-2 py-0.5 text-xs font-bold ${
|
className={`rounded-full px-2 py-0.5 text-xs font-bold ${
|
||||||
index === 0 ? "bg-emerald-200 text-emerald-800" : "bg-blue-200 text-blue-800"
|
index === 0 ? "bg-emerald-200 text-emerald-800" : "bg-blue-200 text-primary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{index === 0 ? "FROM" : "TO"}
|
{index === 0 ? "FROM" : "TO"}
|
||||||
|
|
@ -94,7 +94,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
{/* 연결 화살표 (마지막이 아닌 경우) */}
|
{/* 연결 화살표 (마지막이 아닌 경우) */}
|
||||||
{index < selectedNodes.length - 1 && (
|
{index < selectedNodes.length - 1 && (
|
||||||
<div className="flex justify-center py-1">
|
<div className="flex justify-center py-1">
|
||||||
<div className="text-gray-400">→</div>
|
<div className="text-muted-foreground/70">→</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -104,14 +104,14 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 액션 버튼 */}
|
{/* 액션 버튼 */}
|
||||||
<div className="flex gap-2 border-t border-blue-100 p-3">
|
<div className="flex gap-2 border-t border-primary/10 p-3">
|
||||||
<button
|
<button
|
||||||
onClick={onOpenConnectionModal}
|
onClick={onOpenConnectionModal}
|
||||||
disabled={!canCreateConnection}
|
disabled={!canCreateConnection}
|
||||||
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
|
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
|
||||||
canCreateConnection
|
canCreateConnection
|
||||||
? "bg-accent0 text-white hover:bg-blue-600"
|
? "bg-accent0 text-white hover:bg-primary"
|
||||||
: "cursor-not-allowed bg-gray-300 text-gray-500"
|
: "cursor-not-allowed bg-muted/60 text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>🔗</span>
|
<span>🔗</span>
|
||||||
|
|
@ -119,7 +119,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-muted-foreground hover:bg-gray-300"
|
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-muted/80 px-3 py-2 text-xs font-medium text-muted-foreground hover:bg-muted/60"
|
||||||
>
|
>
|
||||||
<span>🗑️</span>
|
<span>🗑️</span>
|
||||||
<span>초기화</span>
|
<span>초기화</span>
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,13 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
const { table, onColumnClick, onScrollAreaEnter, onScrollAreaLeave, selectedColumns = [] } = data;
|
const { table, onColumnClick, onScrollAreaEnter, onScrollAreaLeave, selectedColumns = [] } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex min-w-[280px] cursor-pointer flex-col overflow-hidden rounded-lg border-2 border-gray-300 bg-white shadow-lg transition-all hover:shadow-xl">
|
<div className="relative flex min-w-[280px] cursor-pointer flex-col overflow-hidden rounded-lg border-2 border-input bg-white shadow-lg transition-all hover:shadow-xl">
|
||||||
{/* React Flow Handles - 숨김 처리 */}
|
{/* React Flow Handles - 숨김 처리 */}
|
||||||
<Handle type="target" position={Position.Left} id="left" className="!invisible !h-1 !w-1" />
|
<Handle type="target" position={Position.Left} id="left" className="!invisible !h-1 !w-1" />
|
||||||
<Handle type="source" position={Position.Right} id="right" className="!invisible !h-1 !w-1" />
|
<Handle type="source" position={Position.Right} id="right" className="!invisible !h-1 !w-1" />
|
||||||
|
|
||||||
{/* 테이블 헤더 - 통일된 디자인 */}
|
{/* 테이블 헤더 - 통일된 디자인 */}
|
||||||
<div className="bg-blue-600 p-3 text-white">
|
<div className="bg-primary p-3 text-white">
|
||||||
<h3 className="truncate text-sm font-semibold">{table.displayName}</h3>
|
<h3 className="truncate text-sm font-semibold">{table.displayName}</h3>
|
||||||
{table.description && <p className="mt-1 truncate text-xs opacity-75">{table.description}</p>}
|
{table.description && <p className="mt-1 truncate text-xs opacity-75">{table.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -56,7 +56,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
<div
|
<div
|
||||||
key={columnKey}
|
key={columnKey}
|
||||||
className={`relative cursor-pointer rounded px-2 py-1 text-xs transition-colors ${
|
className={`relative cursor-pointer rounded px-2 py-1 text-xs transition-colors ${
|
||||||
isSelected ? "bg-primary/20 text-blue-800 ring-2 ring-blue-500" : "text-gray-700 hover:bg-gray-100"
|
isSelected ? "bg-primary/20 text-primary ring-2 ring-ring" : "text-foreground hover:bg-muted"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onColumnClick(table.tableName, columnKey)}
|
onClick={() => onColumnClick(table.tableName, columnKey)}
|
||||||
>
|
>
|
||||||
|
|
@ -64,9 +64,9 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-mono font-medium">{columnDisplayName}</span>
|
<span className="font-mono font-medium">{columnDisplayName}</span>
|
||||||
<span className="text-gray-500">{columnType}</span>
|
<span className="text-muted-foreground">{columnType}</span>
|
||||||
</div>
|
</div>
|
||||||
{column.description && <div className="mt-0.5 text-gray-500">{column.description}</div>}
|
{column.description && <div className="mt-0.5 text-muted-foreground">{column.description}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold text-gray-800">테이블 선택</h3>
|
<h3 className="text-lg font-semibold text-foreground">테이블 선택</h3>
|
||||||
<Button onClick={loadTables} variant="outline" size="sm" disabled={loading}>
|
<Button onClick={loadTables} variant="outline" size="sm" disabled={loading}>
|
||||||
{loading ? "로딩중..." : "새로고침"}
|
{loading ? "로딩중..." : "새로고침"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -82,7 +82,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
||||||
|
|
||||||
{/* 검색 입력 */}
|
{/* 검색 입력 */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="테이블명으로 검색..."
|
placeholder="테이블명으로 검색..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
|
|
@ -99,11 +99,11 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
||||||
<div className="max-h-96 space-y-2 overflow-y-auto">
|
<div className="max-h-96 space-y-2 overflow-y-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="text-sm text-gray-500">테이블 목록을 불러오는 중...</div>
|
<div className="text-sm text-muted-foreground">테이블 목록을 불러오는 중...</div>
|
||||||
</div>
|
</div>
|
||||||
) : filteredTables.length === 0 ? (
|
) : filteredTables.length === 0 ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="text-center text-sm text-gray-500">
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
{searchTerm ? "검색 결과가 없습니다." : "등록된 테이블이 없습니다."}
|
{searchTerm ? "검색 결과가 없습니다." : "등록된 테이블이 없습니다."}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -114,14 +114,14 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
||||||
<Card
|
<Card
|
||||||
key={table.tableName}
|
key={table.tableName}
|
||||||
className={`cursor-pointer transition-all hover:shadow-md ${
|
className={`cursor-pointer transition-all hover:shadow-md ${
|
||||||
isSelected ? "cursor-not-allowed border-primary bg-accent opacity-60" : "hover:border-gray-300"
|
isSelected ? "cursor-not-allowed border-primary bg-accent opacity-60" : "hover:border-input"
|
||||||
}`}
|
}`}
|
||||||
onDoubleClick={() => !isSelected && handleAddTable(table)}
|
onDoubleClick={() => !isSelected && handleAddTable(table)}
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-sm font-medium">{table.displayName}</CardTitle>
|
<CardTitle className="text-sm font-medium">{table.displayName}</CardTitle>
|
||||||
<div className="mt-1 text-xs text-gray-500">{table.columnCount}개 컬럼</div>
|
<div className="mt-1 text-xs text-muted-foreground">{table.columnCount}개 컬럼</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
|
|
@ -132,7 +132,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
||||||
{isSelected && <span className="font-medium text-primary">(추가됨)</span>}
|
{isSelected && <span className="font-medium text-primary">(추가됨)</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{table.description && <p className="line-clamp-2 text-xs text-gray-500">{table.description}</p>}
|
{table.description && <p className="line-clamp-2 text-xs text-muted-foreground">{table.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -142,7 +142,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 통계 정보 */}
|
{/* 통계 정보 */}
|
||||||
<div className="rounded-lg bg-gray-50 p-3 text-xs text-muted-foreground">
|
<div className="rounded-lg bg-muted p-3 text-xs text-muted-foreground">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>전체 테이블: {tables.length}개</span>
|
<span>전체 테이블: {tables.length}개</span>
|
||||||
{searchTerm && <span>검색 결과: {filteredTables.length}개</span>}
|
{searchTerm && <span>검색 결과: {filteredTables.length}개</span>}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{conditions.length === 0 ? (
|
{conditions.length === 0 ? (
|
||||||
<div className="rounded-lg border border-dashed p-3 text-center text-xs text-gray-500">
|
<div className="rounded-lg border border-dashed p-3 text-center text-xs text-muted-foreground">
|
||||||
조건을 추가하면 해당 조건을 만족할 때만 실행됩니다.
|
조건을 추가하면 해당 조건을 만족할 때만 실행됩니다.
|
||||||
<br />
|
<br />
|
||||||
조건이 없으면 항상 실행됩니다.
|
조건이 없으면 항상 실행됩니다.
|
||||||
|
|
@ -92,7 +92,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
||||||
)}
|
)}
|
||||||
{/* 그룹 레벨에 따른 들여쓰기 */}
|
{/* 그룹 레벨에 따른 들여쓰기 */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-accent/50 p-2"
|
className="flex items-center gap-2 rounded border-2 border-dashed border-primary/40 bg-accent/50 p-2"
|
||||||
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
||||||
>
|
>
|
||||||
<span className="font-mono text-sm text-primary">(</span>
|
<span className="font-mono text-sm text-primary">(</span>
|
||||||
|
|
@ -110,7 +110,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
||||||
return (
|
return (
|
||||||
<div key={condition.id} className="flex items-center gap-2">
|
<div key={condition.id} className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-accent/50 p-2"
|
className="flex items-center gap-2 rounded border-2 border-dashed border-primary/40 bg-accent/50 p-2"
|
||||||
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
||||||
>
|
>
|
||||||
<span className="font-mono text-sm text-primary">)</span>
|
<span className="font-mono text-sm text-primary">)</span>
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
|
||||||
value={settings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
|
value={settings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
|
||||||
onValueChange={(value: "AND" | "OR") => updateLogicalOperator(condIndex - 1, value)}
|
onValueChange={(value: "AND" | "OR") => updateLogicalOperator(condIndex - 1, value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 w-24 border-green-200 bg-green-50 text-xs">
|
<SelectTrigger className="h-6 w-24 border-emerald-200 bg-emerald-50 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -124,11 +124,11 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-green-50/50 p-1"
|
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-emerald-50/50 p-1"
|
||||||
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
|
||||||
>
|
>
|
||||||
<span className="font-mono text-xs text-green-600">(</span>
|
<span className="font-mono text-xs text-emerald-600">(</span>
|
||||||
<span className="text-xs text-green-600">그룹 시작</span>
|
<span className="text-xs text-emerald-600">그룹 시작</span>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -147,11 +147,11 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-green-50/50 p-1"
|
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-emerald-50/50 p-1"
|
||||||
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
|
||||||
>
|
>
|
||||||
<span className="font-mono text-xs text-green-600">)</span>
|
<span className="font-mono text-xs text-emerald-600">)</span>
|
||||||
<span className="text-xs text-green-600">그룹 끝</span>
|
<span className="text-xs text-emerald-600">그룹 끝</span>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -174,7 +174,7 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
|
||||||
value={settings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
|
value={settings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
|
||||||
onValueChange={(value: "AND" | "OR") => updateLogicalOperator(condIndex - 1, value)}
|
onValueChange={(value: "AND" | "OR") => updateLogicalOperator(condIndex - 1, value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-6 w-24 border-green-200 bg-green-50 text-xs">
|
<SelectTrigger className="h-6 w-24 border-emerald-200 bg-emerald-50 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -82,21 +82,21 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<details className="group">
|
<details className="group">
|
||||||
<summary
|
<summary
|
||||||
className={`flex cursor-pointer items-center justify-between rounded border p-2 text-xs font-medium hover:bg-gray-50 hover:text-gray-900 ${
|
className={`flex cursor-pointer items-center justify-between rounded border p-2 text-xs font-medium hover:bg-muted hover:text-foreground ${
|
||||||
isConditionRequired && !hasValidConditions
|
isConditionRequired && !hasValidConditions
|
||||||
? "border-red-300 bg-destructive/10 text-red-700"
|
? "border-destructive/30 bg-destructive/10 text-destructive"
|
||||||
: "border-gray-200 text-gray-700"
|
: "border-border text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
🔍 이 액션의 실행 조건
|
🔍 이 액션의 실행 조건
|
||||||
{isConditionRequired ? (
|
{isConditionRequired ? (
|
||||||
<span className="rounded bg-destructive/20 px-1 py-0.5 text-xs font-semibold text-red-700">필수</span>
|
<span className="rounded bg-destructive/20 px-1 py-0.5 text-xs font-semibold text-destructive">필수</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-500">(선택사항)</span>
|
<span className="text-muted-foreground">(선택사항)</span>
|
||||||
)}
|
)}
|
||||||
{action.conditions && action.conditions.length > 0 && (
|
{action.conditions && action.conditions.length > 0 && (
|
||||||
<span className="rounded-full bg-primary/20 px-2 py-0.5 text-xs text-blue-700">
|
<span className="rounded-full bg-primary/20 px-2 py-0.5 text-xs text-primary">
|
||||||
{action.conditions.length}개
|
{action.conditions.length}개
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -113,14 +113,14 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
clearAllConditions();
|
clearAllConditions();
|
||||||
}}
|
}}
|
||||||
className="h-5 w-5 p-0 text-red-500 hover:text-red-700"
|
className="h-5 w-5 p-0 text-destructive hover:text-destructive"
|
||||||
title="조건 모두 삭제"
|
title="조건 모두 삭제"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</summary>
|
</summary>
|
||||||
<div className="mt-2 space-y-2 border-l-2 border-gray-100 pl-4">
|
<div className="mt-2 space-y-2 border-l-2 border-border pl-4">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button size="sm" variant="outline" onClick={addActionCondition} className="h-6 text-xs">
|
<Button size="sm" variant="outline" onClick={addActionCondition} className="h-6 text-xs">
|
||||||
|
|
@ -151,13 +151,13 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
|
||||||
<div
|
<div
|
||||||
className={`rounded border p-3 text-xs ${
|
className={`rounded border p-3 text-xs ${
|
||||||
isConditionRequired
|
isConditionRequired
|
||||||
? "border-destructive/20 bg-destructive/10 text-red-700"
|
? "border-destructive/20 bg-destructive/10 text-destructive"
|
||||||
: "border-gray-200 bg-gray-50 text-muted-foreground"
|
: "border-border bg-muted text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isConditionRequired ? (
|
{isConditionRequired ? (
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<span className="text-red-500">⚠️</span>
|
<span className="text-destructive">⚠️</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">실행조건이 필요합니다</div>
|
<div className="font-medium">실행조건이 필요합니다</div>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue