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,
|
||||
{ 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_LAYOUT: { label: "레이아웃", icon: Monitor, color: "bg-purple-100 text-purple-700" },
|
||||
FLOW: { label: "플로우", icon: GitBranch, color: "bg-green-100 text-green-700" },
|
||||
FLOW_STEP: { label: "플로우 스텝", icon: GitBranch, color: "bg-green-100 text-green-700" },
|
||||
USER: { label: "사용자", icon: User, color: "bg-orange-100 text-orange-700" },
|
||||
ROLE: { label: "권한", icon: Shield, color: "bg-red-100 text-red-700" },
|
||||
PERMISSION: { label: "권한", icon: Shield, color: "bg-red-100 text-red-700" },
|
||||
FLOW: { label: "플로우", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" },
|
||||
FLOW_STEP: { label: "플로우 스텝", icon: GitBranch, color: "bg-emerald-100 text-emerald-700" },
|
||||
USER: { label: "사용자", icon: User, color: "bg-amber-100 text-orange-700" },
|
||||
ROLE: { label: "권한", icon: Shield, color: "bg-destructive/10 text-destructive" },
|
||||
PERMISSION: { label: "권한", icon: Shield, color: "bg-destructive/10 text-destructive" },
|
||||
COMPANY: { label: "회사", icon: Building2, color: "bg-indigo-100 text-indigo-700" },
|
||||
CODE_CATEGORY: { 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" },
|
||||
TABLE: { 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-muted text-foreground" },
|
||||
NUMBERING_RULE: { label: "채번 규칙", icon: FileText, color: "bg-amber-100 text-amber-700" },
|
||||
BATCH: { label: "배치", icon: RefreshCw, color: "bg-teal-100 text-teal-700" },
|
||||
};
|
||||
|
||||
const ACTION_CONFIG: Record<string, { label: string; color: string }> = {
|
||||
CREATE: { label: "생성", color: "bg-emerald-100 text-emerald-700" },
|
||||
UPDATE: { label: "수정", color: "bg-blue-100 text-blue-700" },
|
||||
DELETE: { label: "삭제", color: "bg-red-100 text-red-700" },
|
||||
UPDATE: { label: "수정", color: "bg-primary/10 text-primary" },
|
||||
DELETE: { label: "삭제", color: "bg-destructive/10 text-destructive" },
|
||||
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" },
|
||||
BATCH_CREATE: { label: "배치생성", color: "bg-emerald-100 text-emerald-700" },
|
||||
BATCH_UPDATE: { label: "배치수정", color: "bg-blue-100 text-blue-700" },
|
||||
BATCH_DELETE: { label: "배치삭제", color: "bg-red-100 text-red-700" },
|
||||
BATCH_UPDATE: { label: "배치수정", color: "bg-primary/10 text-primary" },
|
||||
BATCH_DELETE: { label: "배치삭제", color: "bg-destructive/10 text-destructive" },
|
||||
};
|
||||
|
||||
function formatDateTime(dateStr: string): string {
|
||||
|
|
@ -203,12 +203,12 @@ function renderChanges(changes: Record<string, unknown>) {
|
|||
<tr className="bg-muted/50">
|
||||
<th className="px-3 py-1.5 text-left font-medium">항목</th>
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
|
|
@ -234,7 +234,7 @@ function renderChanges(changes: Record<string, unknown>) {
|
|||
{hasBefore && (
|
||||
<td className="px-3 py-1.5">
|
||||
{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}
|
||||
</span>
|
||||
) : (
|
||||
|
|
@ -245,7 +245,7 @@ function renderChanges(changes: Record<string, unknown>) {
|
|||
{hasAfter && (
|
||||
<td className="px-3 py-1.5">
|
||||
{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}
|
||||
</span>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -311,10 +311,10 @@ export default function BatchCreatePage() {
|
|||
{/* 매핑 설정 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* FROM 섹션 */}
|
||||
<Card className="border-green-200">
|
||||
<CardHeader className="bg-green-50">
|
||||
<CardTitle className="text-green-700">FROM (원본 데이터베이스)</CardTitle>
|
||||
<p className="text-sm text-green-600">
|
||||
<Card className="border-emerald-200">
|
||||
<CardHeader className="bg-emerald-50">
|
||||
<CardTitle className="text-emerald-700">FROM (원본 데이터베이스)</CardTitle>
|
||||
<p className="text-sm text-emerald-600">
|
||||
1단계: 커넥션을 선택하세요 → 2단계: 테이블을 선택하세요 → 3단계: 컬럼을 클릭해서 매핑하세요
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
|
@ -365,7 +365,7 @@ export default function BatchCreatePage() {
|
|||
{/* FROM 컬럼 목록 */}
|
||||
{fromTable && (
|
||||
<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">
|
||||
{fromColumns.map((column) => (
|
||||
<div
|
||||
|
|
@ -373,16 +373,16 @@ export default function BatchCreatePage() {
|
|||
onClick={() => handleFromColumnClick(column)}
|
||||
className={`p-3 border rounded cursor-pointer transition-colors ${
|
||||
selectedFromColumn?.column_name === column.column_name
|
||||
? 'bg-green-100 border-green-300'
|
||||
: 'hover:bg-gray-50 border-gray-200'
|
||||
? 'bg-emerald-100 border-green-300'
|
||||
: 'hover:bg-muted border-border'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
{fromColumns.length === 0 && fromTable && (
|
||||
<div className="text-center text-gray-500 py-4">
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
컬럼을 불러오는 중...
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -393,10 +393,10 @@ export default function BatchCreatePage() {
|
|||
</Card>
|
||||
|
||||
{/* TO 섹션 */}
|
||||
<Card className="border-red-200">
|
||||
<CardHeader className="bg-red-50">
|
||||
<CardTitle className="text-red-700">TO (대상 데이터베이스)</CardTitle>
|
||||
<p className="text-sm text-red-600">
|
||||
<Card className="border-destructive/20">
|
||||
<CardHeader className="bg-destructive/10">
|
||||
<CardTitle className="text-destructive">TO (대상 데이터베이스)</CardTitle>
|
||||
<p className="text-sm text-destructive">
|
||||
FROM에서 컬럼을 선택한 후, 여기서 대상 컬럼을 클릭하면 매핑됩니다
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
|
@ -447,7 +447,7 @@ export default function BatchCreatePage() {
|
|||
{/* TO 컬럼 목록 */}
|
||||
{toTable && (
|
||||
<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">
|
||||
{toColumns.map((column) => (
|
||||
<div
|
||||
|
|
@ -455,16 +455,16 @@ export default function BatchCreatePage() {
|
|||
onClick={() => handleToColumnClick(column)}
|
||||
className={`p-3 border rounded cursor-pointer transition-colors ${
|
||||
selectedFromColumn
|
||||
? 'hover:bg-red-50 border-gray-200'
|
||||
: 'bg-gray-100 border-gray-300 cursor-not-allowed'
|
||||
? 'hover:bg-destructive/10 border-border'
|
||||
: 'bg-muted border-input cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
{toColumns.length === 0 && toTable && (
|
||||
<div className="text-center text-gray-500 py-4">
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
컬럼을 불러오는 중...
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -484,22 +484,22 @@ export default function BatchCreatePage() {
|
|||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{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="text-sm">
|
||||
<div className="font-medium">
|
||||
{mapping.from_table_name}.{mapping.from_column_name}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
<div className="text-muted-foreground">
|
||||
{mapping.from_column_type}
|
||||
</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="font-medium">
|
||||
{mapping.to_table_name}.{mapping.to_column_name}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
<div className="text-muted-foreground">
|
||||
{mapping.to_column_type}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -508,7 +508,7 @@ export default function BatchCreatePage() {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeMapping(index)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -815,7 +815,7 @@ export default function BatchEditPage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{authTokenMode === "direct"
|
||||
? "API 호출 시 Authorization 헤더에 사용할 토큰을 입력하세요."
|
||||
: "auth_tokens 테이블에서 선택한 서비스의 최신 토큰을 사용합니다."}
|
||||
|
|
@ -874,7 +874,7 @@ export default function BatchEditPage() {
|
|||
onChange={(e) => setDataArrayPath(e.target.value)}
|
||||
placeholder="response (예: data.items, results)"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
API 응답에서 배열 데이터가 있는 경로를 입력하세요. 비워두면 응답 전체를 사용합니다.
|
||||
<br />
|
||||
예시: response, data.items, result.list
|
||||
|
|
@ -902,7 +902,7 @@ export default function BatchEditPage() {
|
|||
className="min-h-[100px]"
|
||||
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>
|
||||
)}
|
||||
|
||||
|
|
@ -910,7 +910,7 @@ export default function BatchEditPage() {
|
|||
<div className="space-y-4">
|
||||
<div className="border-t pt-4">
|
||||
<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>
|
||||
|
|
@ -967,26 +967,26 @@ export default function BatchEditPage() {
|
|||
}
|
||||
/>
|
||||
{apiParamSource === "dynamic" && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
동적값은 배치 실행 시 설정된 값으로 치환됩니다. 예: {"{{user_id}}"} → 실제 사용자 ID
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{apiParamType === "url" && (
|
||||
<div className="rounded-lg bg-blue-50 p-3">
|
||||
<div className="text-sm font-medium text-blue-800">URL 파라미터 예시</div>
|
||||
<div className="mt-1 text-sm text-blue-700">
|
||||
<div className="rounded-lg bg-primary/10 p-3">
|
||||
<div className="text-sm font-medium text-primary">URL 파라미터 예시</div>
|
||||
<div className="mt-1 text-sm text-primary">
|
||||
엔드포인트: /api/users/{`{${apiParamName || "userId"}}`}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{apiParamType === "query" && (
|
||||
<div className="rounded-lg bg-green-50 p-3">
|
||||
<div className="text-sm font-medium text-green-800">쿼리 파라미터 예시</div>
|
||||
<div className="mt-1 text-sm text-green-700">
|
||||
<div className="rounded-lg bg-emerald-50 p-3">
|
||||
<div className="text-sm font-medium text-emerald-800">쿼리 파라미터 예시</div>
|
||||
<div className="mt-1 text-sm text-emerald-700">
|
||||
실제 호출: {mappings[0]?.from_table_name || "/api/users"}?{apiParamName || "userId"}=
|
||||
{apiParamValue || "123"}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ export default function FlowEditorPage() {
|
|||
onNodeDragStop={handleNodeDragStop}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
className="bg-gray-50"
|
||||
className="bg-muted"
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
|
|
|
|||
|
|
@ -996,7 +996,7 @@ export default function FlowManagementPage() {
|
|||
<div className="flex flex-col">
|
||||
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||||
{table.description && (
|
||||
<span className="text-[10px] text-gray-500">{table.description}</span>
|
||||
<span className="text-[10px] text-muted-foreground">{table.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ example2@example.com,김철수,XYZ회사`;
|
|||
{recipients.length > 0 && (
|
||||
<div className="rounded-md border bg-muted p-4">
|
||||
<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>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -101,8 +101,8 @@ export default function MailDashboardPage() {
|
|||
value: stats.totalAccounts,
|
||||
icon: Users,
|
||||
color: "blue",
|
||||
bgColor: "bg-blue-100",
|
||||
iconColor: "text-blue-600",
|
||||
bgColor: "bg-primary/10",
|
||||
iconColor: "text-primary",
|
||||
href: "/admin/mail/accounts",
|
||||
},
|
||||
{
|
||||
|
|
@ -110,8 +110,8 @@ export default function MailDashboardPage() {
|
|||
value: stats.totalTemplates,
|
||||
icon: FileText,
|
||||
color: "green",
|
||||
bgColor: "bg-green-100",
|
||||
iconColor: "text-green-600",
|
||||
bgColor: "bg-emerald-100",
|
||||
iconColor: "text-emerald-600",
|
||||
href: "/admin/mail/templates",
|
||||
},
|
||||
{
|
||||
|
|
@ -119,8 +119,8 @@ export default function MailDashboardPage() {
|
|||
value: stats.sentToday,
|
||||
icon: Send,
|
||||
color: "orange",
|
||||
bgColor: "bg-orange-100",
|
||||
iconColor: "text-orange-600",
|
||||
bgColor: "bg-amber-100",
|
||||
iconColor: "text-amber-600",
|
||||
href: "/admin/mail/sent",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -438,8 +438,8 @@ export default function MailReceivePage() {
|
|||
<div
|
||||
className={`mt-4 p-3 rounded-lg flex items-center gap-2 ${
|
||||
testResult.success
|
||||
? "bg-green-50 text-green-800 border border-green-200"
|
||||
: "bg-red-50 text-red-800 border border-red-200"
|
||||
? "bg-emerald-50 text-emerald-800 border border-emerald-200"
|
||||
: "bg-destructive/10 text-red-800 border border-destructive/20"
|
||||
}`}
|
||||
>
|
||||
{testResult.success ? (
|
||||
|
|
@ -460,7 +460,7 @@ export default function MailReceivePage() {
|
|||
<div className="flex flex-col md:flex-row gap-3">
|
||||
{/* 검색 */}
|
||||
<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
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
|
|
@ -511,7 +511,7 @@ export default function MailReceivePage() {
|
|||
{filteredAndSortedMails.length}개의 메일이 검색되었습니다
|
||||
{searchTerm && (
|
||||
<span className="ml-2">
|
||||
(검색어: <span className="font-medium text-orange-600">{searchTerm}</span>)
|
||||
(검색어: <span className="font-medium text-amber-600">{searchTerm}</span>)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -527,14 +527,14 @@ export default function MailReceivePage() {
|
|||
{loading ? (
|
||||
<Card className="">
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : filteredAndSortedMails.length === 0 ? (
|
||||
<Card className="text-center py-16 bg-card ">
|
||||
<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">
|
||||
{!selectedAccountId
|
||||
? "메일 계정을 선택하세요"
|
||||
|
|
@ -560,9 +560,9 @@ export default function MailReceivePage() {
|
|||
</Card>
|
||||
) : (
|
||||
<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">
|
||||
<Inbox className="w-5 h-5 text-orange-500" />
|
||||
<Inbox className="w-5 h-5 text-amber-500" />
|
||||
받은 메일함 ({filteredAndSortedMails.length}/{mails.length}개)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -573,14 +573,14 @@ export default function MailReceivePage() {
|
|||
key={mail.id}
|
||||
onClick={() => handleMailClick(mail)}
|
||||
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" : ""}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* 읽음 표시 */}
|
||||
<div className="flex-shrink-0 w-2 h-2 mt-2">
|
||||
{!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>
|
||||
|
||||
|
|
@ -598,7 +598,7 @@ export default function MailReceivePage() {
|
|||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{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">
|
||||
{formatDate(mail.date)}
|
||||
|
|
@ -882,14 +882,14 @@ export default function MailReceivePage() {
|
|||
) : loadingDetail ? (
|
||||
<Card className="sticky top-6">
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="sticky top-6">
|
||||
<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>
|
||||
|
|
@ -900,10 +900,10 @@ export default function MailReceivePage() {
|
|||
</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>
|
||||
<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>
|
||||
</CardHeader>
|
||||
|
|
@ -913,81 +913,81 @@ export default function MailReceivePage() {
|
|||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<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">
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>IMAP 프로토콜 메일 수신</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>메일 목록 표시</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>읽음/안읽음 상태</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>첨부파일 유무 표시</span>
|
||||
</li>
|
||||
</ul>
|
||||
</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">
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>HTML 본문 렌더링</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>텍스트 본문 보기</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>자동 읽음 처리</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>첨부파일 다운로드</span>
|
||||
</li>
|
||||
</ul>
|
||||
</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">
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>통합 검색 (제목/발신자/내용)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>필터링 (읽음/첨부파일)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>정렬 (날짜/발신자)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>자동 새로고침 (30초)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</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">
|
||||
<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>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>비밀번호 암호화</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-500 mr-2">✓</span>
|
||||
<span className="text-emerald-500 mr-2">✓</span>
|
||||
<span>안전한 파일명 생성</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -516,12 +516,12 @@ ${data.originalBody}`;
|
|||
toast({
|
||||
title: (
|
||||
<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>
|
||||
</div>
|
||||
) as any,
|
||||
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({
|
||||
title: (
|
||||
<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>
|
||||
</div>
|
||||
) 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>
|
||||
{new Date(lastSaved).toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
|
|
@ -895,7 +895,7 @@ ${data.originalBody}`;
|
|||
{to.map((email) => (
|
||||
<div
|
||||
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>
|
||||
<button
|
||||
|
|
@ -933,12 +933,12 @@ ${data.originalBody}`;
|
|||
{cc.map((email) => (
|
||||
<div
|
||||
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>
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
|
|
@ -1222,7 +1222,7 @@ ${data.originalBody}`;
|
|||
<div
|
||||
key={component.id}
|
||||
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>
|
||||
|
|
@ -1236,7 +1236,7 @@ ${data.originalBody}`;
|
|||
{component.logoSrc && <img src={component.logoSrc} alt="로고" className="h-10" />}
|
||||
<span className="font-bold text-lg">{component.brandName}</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{component.sendDate}</span>
|
||||
<span className="text-sm text-muted-foreground">{component.sendDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1245,13 +1245,13 @@ ${data.originalBody}`;
|
|||
return (
|
||||
<div key={component.id} className="border rounded-lg overflow-hidden">
|
||||
{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">
|
||||
<tbody>
|
||||
{component.rows?.map((row: any, i: number) => (
|
||||
<tr key={i} className={i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-4 py-2 font-medium text-gray-600 w-1/3 border-r">{row.label}</td>
|
||||
<tr key={i} className={i % 2 === 0 ? 'bg-white' : 'bg-muted'}>
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -1263,9 +1263,9 @@ ${data.originalBody}`;
|
|||
case 'alertBox':
|
||||
return (
|
||||
<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 === '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'
|
||||
}`}>
|
||||
{component.alertTitle && <div className="font-bold mb-1">{component.alertTitle}</div>}
|
||||
|
|
@ -1275,13 +1275,13 @@ ${data.originalBody}`;
|
|||
|
||||
case 'divider':
|
||||
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':
|
||||
return (
|
||||
<div key={component.id} className="text-center text-sm text-gray-500 py-4 border-t bg-gray-50">
|
||||
{component.companyName && <div className="font-semibold text-gray-700">{component.companyName}</div>}
|
||||
<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-foreground">{component.companyName}</div>}
|
||||
{(component.ceoName || component.businessNumber) && (
|
||||
<div className="mt-1">
|
||||
{component.ceoName && <span>대표: {component.ceoName}</span>}
|
||||
|
|
@ -1297,7 +1297,7 @@ ${data.originalBody}`;
|
|||
{component.email && <span>Email: {component.email}</span>}
|
||||
</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>
|
||||
);
|
||||
|
||||
|
|
@ -1318,9 +1318,9 @@ ${data.originalBody}`;
|
|||
}
|
||||
})}
|
||||
</div>
|
||||
<div className="bg-gradient-to-r from-green-50 to-emerald-50 px-4 py-3 border-t border-green-200">
|
||||
<p className="text-sm text-green-800 flex items-center gap-2 font-medium">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-600" />
|
||||
<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-emerald-800 flex items-center gap-2 font-medium">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
||||
위 내용으로 메일이 발송됩니다
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -1396,7 +1396,7 @@ ${data.originalBody}`;
|
|||
onChange={handleFileSelect}
|
||||
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>
|
||||
|
|
@ -1430,7 +1430,7 @@ ${data.originalBody}`;
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
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" />
|
||||
</Button>
|
||||
|
|
@ -1530,7 +1530,7 @@ ${data.originalBody}`;
|
|||
<div key={index} className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<File className="w-3 h-3" />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -371,8 +371,8 @@ export default function SentMailPage() {
|
|||
{stats.successCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-green-500/10 rounded-lg">
|
||||
<CheckCircle2 className="w-6 h-6 text-green-600" />
|
||||
<div className="p-3 bg-emerald-500/10 rounded-lg">
|
||||
<CheckCircle2 className="w-6 h-6 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -387,8 +387,8 @@ export default function SentMailPage() {
|
|||
{stats.failedCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-red-500/10 rounded-lg">
|
||||
<XCircle className="w-6 h-6 text-red-600" />
|
||||
<div className="p-3 bg-destructive/10 rounded-lg">
|
||||
<XCircle className="w-6 h-6 text-destructive" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -403,8 +403,8 @@ export default function SentMailPage() {
|
|||
{stats.todayCount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-500/10 rounded-lg">
|
||||
<Calendar className="w-6 h-6 text-blue-600" />
|
||||
<div className="p-3 bg-primary/10 rounded-lg">
|
||||
<Calendar className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -592,19 +592,19 @@ export default function BatchManagementNewPage() {
|
|||
<div
|
||||
key={option.value}
|
||||
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)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{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 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>
|
||||
|
|
@ -739,7 +739,7 @@ export default function BatchManagementNewPage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{authTokenMode === "direct"
|
||||
? "API 호출 시 Authorization 헤더에 사용할 토큰을 입력하세요."
|
||||
: "auth_tokens 테이블에서 선택한 서비스의 최신 토큰을 사용합니다."}
|
||||
|
|
@ -782,7 +782,7 @@ export default function BatchManagementNewPage() {
|
|||
onChange={(e) => setDataArrayPath(e.target.value)}
|
||||
placeholder="response (예: data.items, results)"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
API 응답에서 배열 데이터가 있는 경로를 입력하세요. 비워두면 응답 전체를 사용합니다.
|
||||
<br />
|
||||
예시: response, data.items, result.list
|
||||
|
|
@ -801,7 +801,7 @@ export default function BatchManagementNewPage() {
|
|||
className="min-h-[100px]"
|
||||
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>
|
||||
)}
|
||||
|
||||
|
|
@ -809,7 +809,7 @@ export default function BatchManagementNewPage() {
|
|||
<div className="space-y-4">
|
||||
<div className="border-t pt-4">
|
||||
<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>
|
||||
|
|
@ -868,26 +868,26 @@ export default function BatchManagementNewPage() {
|
|||
}
|
||||
/>
|
||||
{apiParamSource === "dynamic" && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
동적값은 배치 실행 시 설정된 값으로 치환됩니다. 예: {"{{user_id}}"} → 실제 사용자 ID
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{apiParamType === "url" && (
|
||||
<div className="rounded-lg bg-blue-50 p-3">
|
||||
<div className="text-sm font-medium text-blue-800">URL 파라미터 예시</div>
|
||||
<div className="mt-1 text-sm text-blue-700">
|
||||
<div className="rounded-lg bg-primary/10 p-3">
|
||||
<div className="text-sm font-medium text-primary">URL 파라미터 예시</div>
|
||||
<div className="mt-1 text-sm text-primary">
|
||||
엔드포인트: /api/users/{`{${apiParamName || "userId"}}`}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{apiParamType === "query" && (
|
||||
<div className="rounded-lg bg-green-50 p-3">
|
||||
<div className="text-sm font-medium text-green-800">쿼리 파라미터 예시</div>
|
||||
<div className="mt-1 text-sm text-green-700">
|
||||
<div className="rounded-lg bg-emerald-50 p-3">
|
||||
<div className="text-sm font-medium text-emerald-800">쿼리 파라미터 예시</div>
|
||||
<div className="mt-1 text-sm text-emerald-700">
|
||||
실제 호출: {fromEndpoint || "/api/users"}?{apiParamName || "userId"}=
|
||||
{apiParamValue || "123"}
|
||||
</div>
|
||||
|
|
@ -899,9 +899,9 @@ export default function BatchManagementNewPage() {
|
|||
|
||||
{/* API 호출 미리보기 정보 */}
|
||||
{fromApiUrl && fromEndpoint && (
|
||||
<div className="rounded-lg bg-gray-50 p-3">
|
||||
<div className="text-sm font-medium text-gray-700">API 호출 정보</div>
|
||||
<div className="mt-1 text-sm text-gray-600">
|
||||
<div className="rounded-lg bg-muted p-3">
|
||||
<div className="text-sm font-medium text-foreground">API 호출 정보</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
{fromApiMethod} {fromApiUrl}
|
||||
{apiParamType === "url" && apiParamName && apiParamValue
|
||||
? fromEndpoint.replace(`{${apiParamName}}`, apiParamValue) || fromEndpoint + `/${apiParamValue}`
|
||||
|
|
@ -911,14 +911,14 @@ export default function BatchManagementNewPage() {
|
|||
: ""}
|
||||
</div>
|
||||
{((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"
|
||||
? `Authorization: Bearer ${fromApiKey.substring(0, 15)}...`
|
||||
: `Authorization: DB 토큰 (${authServiceName})`}
|
||||
</div>
|
||||
)}
|
||||
{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" ? "고정값" : "동적값"})
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -988,7 +988,7 @@ export default function BatchManagementNewPage() {
|
|||
setSelectedColumns(selectedColumns.filter((col) => col !== column.column_name));
|
||||
}
|
||||
}}
|
||||
className="rounded border-gray-300"
|
||||
className="rounded border-input"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`col-${column.column_name}`}
|
||||
|
|
@ -1002,14 +1002,14 @@ export default function BatchManagementNewPage() {
|
|||
</div>
|
||||
|
||||
{/* 선택된 컬럼 개수 표시 */}
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
선택된 컬럼: {selectedColumns.length}개 / 전체: {fromColumns.length}개
|
||||
</div>
|
||||
|
||||
{/* 빠른 매핑 버튼들 */}
|
||||
{selectedColumns.length > 0 && toApiFields.length > 0 && (
|
||||
<div className="mt-3 rounded-lg border border-green-200 bg-green-50 p-3">
|
||||
<div className="mb-2 text-sm font-medium text-green-800">빠른 매핑</div>
|
||||
<div className="mt-3 rounded-lg border border-emerald-200 bg-emerald-50 p-3">
|
||||
<div className="mb-2 text-sm font-medium text-emerald-800">빠른 매핑</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1051,7 +1051,7 @@ export default function BatchManagementNewPage() {
|
|||
setDbToApiFieldMapping(mapping);
|
||||
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>
|
||||
|
|
@ -1061,7 +1061,7 @@ export default function BatchManagementNewPage() {
|
|||
setDbToApiFieldMapping({});
|
||||
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>
|
||||
|
|
@ -1071,9 +1071,9 @@ export default function BatchManagementNewPage() {
|
|||
|
||||
{/* 자동 생성된 JSON 미리보기 */}
|
||||
{selectedColumns.length > 0 && (
|
||||
<div className="mt-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div className="mb-2 text-sm font-medium text-blue-800">자동 생성된 JSON 구조</div>
|
||||
<pre className="overflow-x-auto font-mono text-xs text-blue-600">
|
||||
<div className="mt-3 rounded-lg border border-primary/20 bg-primary/10 p-3">
|
||||
<div className="mb-2 text-sm font-medium text-primary">자동 생성된 JSON 구조</div>
|
||||
<pre className="overflow-x-auto font-mono text-xs text-primary">
|
||||
{JSON.stringify(
|
||||
selectedColumns.reduce(
|
||||
(obj, col) => {
|
||||
|
|
@ -1105,7 +1105,7 @@ export default function BatchManagementNewPage() {
|
|||
setToApiBody(autoJson);
|
||||
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에 적용
|
||||
</button>
|
||||
|
|
@ -1231,7 +1231,7 @@ export default function BatchManagementNewPage() {
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
이 컬럼 값이 같으면 UPDATE, 없으면 INSERT 합니다. (예: device_serial_number)
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -1273,7 +1273,7 @@ export default function BatchManagementNewPage() {
|
|||
placeholder="/api/users"
|
||||
/>
|
||||
{(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}"}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -1310,7 +1310,7 @@ export default function BatchManagementNewPage() {
|
|||
))}
|
||||
</SelectContent>
|
||||
</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)
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -1321,7 +1321,7 @@ export default function BatchManagementNewPage() {
|
|||
<button
|
||||
type="button"
|
||||
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" />
|
||||
<span>API 필드 미리보기</span>
|
||||
|
|
@ -1330,13 +1330,13 @@ export default function BatchManagementNewPage() {
|
|||
|
||||
{/* TO API 필드 표시 */}
|
||||
{toApiFields.length > 0 && (
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 p-3">
|
||||
<div className="mb-2 text-sm font-medium text-green-800">
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3">
|
||||
<div className="mb-2 text-sm font-medium text-emerald-800">
|
||||
API 필드 목록 ({toApiFields.length}개)
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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}
|
||||
</span>
|
||||
))}
|
||||
|
|
@ -1355,7 +1355,7 @@ export default function BatchManagementNewPage() {
|
|||
placeholder='{"id": "{{id}}", "name": "{{name}}", "email": "{{email}}"}'
|
||||
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}}"}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1363,15 +1363,15 @@ export default function BatchManagementNewPage() {
|
|||
|
||||
{/* API 호출 정보 */}
|
||||
{toApiUrl && toApiKey && toEndpoint && (
|
||||
<div className="rounded-lg bg-gray-50 p-3">
|
||||
<div className="text-sm font-medium text-gray-700">API 호출 정보</div>
|
||||
<div className="mt-1 text-sm text-gray-600">
|
||||
<div className="rounded-lg bg-muted p-3">
|
||||
<div className="text-sm font-medium text-foreground">API 호출 정보</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
{toApiMethod} {toApiUrl}
|
||||
{toEndpoint}
|
||||
</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 && (
|
||||
<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>
|
||||
)}
|
||||
|
|
@ -1394,7 +1394,7 @@ export default function BatchManagementNewPage() {
|
|||
데이터 불러오고 매핑하기
|
||||
</Button>
|
||||
{(!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 섹션의 필수 값을 모두 입력해야 합니다.
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -1693,17 +1693,17 @@ const DbToRestApiMappingCard = memo(function DbToRestApiMappingCard({
|
|||
<CardContent>
|
||||
<div className="max-h-96 space-y-3 overflow-y-auto rounded-lg border p-4">
|
||||
{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 컬럼 정보 */}
|
||||
<div className="flex-1">
|
||||
<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"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 화살표 */}
|
||||
<div className="text-gray-400">→</div>
|
||||
<div className="text-muted-foreground/70">→</div>
|
||||
|
||||
{/* API 필드 선택 드롭다운 */}
|
||||
<div className="flex-1">
|
||||
|
|
@ -1735,7 +1735,7 @@ const DbToRestApiMappingCard = memo(function DbToRestApiMappingCard({
|
|||
<input
|
||||
type="text"
|
||||
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) => {
|
||||
setDbToApiFieldMapping((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]
|
||||
? `매핑: ${column.column_name} → ${dbToApiFieldMapping[column.column_name]}`
|
||||
: `기본값: ${column.column_name} (DB 컬럼명 사용)`}
|
||||
|
|
@ -1755,32 +1755,32 @@ const DbToRestApiMappingCard = memo(function DbToRestApiMappingCard({
|
|||
{/* 템플릿 미리보기 */}
|
||||
<div className="flex-1">
|
||||
<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>
|
||||
|
||||
{selectedColumns.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div className="text-sm font-medium text-blue-800">자동 생성된 JSON 구조</div>
|
||||
<pre className="mt-1 overflow-x-auto font-mono text-xs text-blue-600">{autoJsonPreview}</pre>
|
||||
<div className="mt-4 rounded-lg border border-primary/20 bg-primary/10 p-3">
|
||||
<div className="text-sm font-medium text-primary">자동 생성된 JSON 구조</div>
|
||||
<pre className="mt-1 overflow-x-auto font-mono text-xs text-primary">{autoJsonPreview}</pre>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setToApiBody(autoJsonPreview);
|
||||
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에 적용
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div className="text-sm font-medium text-blue-800">매핑 사용 예시</div>
|
||||
<div className="mt-1 font-mono text-xs text-blue-600">
|
||||
<div className="mt-4 rounded-lg border border-primary/20 bg-primary/10 p-3">
|
||||
<div className="text-sm font-medium text-primary">매핑 사용 예시</div>
|
||||
<div className="mt-1 font-mono text-xs text-primary">
|
||||
{'{"id": "{{id}}", "name": "{{user_name}}", "email": "{{email}}"}'}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ export default function AutoFillTab() {
|
|||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<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>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -676,7 +676,7 @@ export default function AutoFillTab() {
|
|||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-destructive hover:bg-destructive">
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -577,9 +577,9 @@ export default function CascadingRelationsTab() {
|
|||
</TableCell>
|
||||
<TableCell>
|
||||
<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" />
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -619,7 +619,7 @@ export default function CascadingRelationsTab() {
|
|||
<div className="space-y-4">
|
||||
{/* Step 1: 부모 테이블 */}
|
||||
<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="space-y-1.5">
|
||||
<Label className="text-xs">테이블</Label>
|
||||
|
|
@ -696,7 +696,7 @@ export default function CascadingRelationsTab() {
|
|||
|
||||
{/* Step 2: 자식 테이블 */}
|
||||
<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="space-y-1.5">
|
||||
<Label className="text-xs">테이블</Label>
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ export default function ConditionTab() {
|
|||
<TableCell>
|
||||
<div className="text-sm">
|
||||
<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>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
|
@ -329,7 +329,7 @@ export default function ConditionTab() {
|
|||
size="icon"
|
||||
onClick={() => handleDeleteConfirm(condition.conditionId!)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -491,7 +491,7 @@ export default function ConditionTab() {
|
|||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-destructive hover:bg-destructive">
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -501,7 +501,7 @@ export default function HierarchyTab() {
|
|||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -548,7 +548,7 @@ export default function HierarchyTab() {
|
|||
size="icon"
|
||||
onClick={() => handleDeleteLevel(level.levelId!, group.groupCode)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -836,7 +836,7 @@ export default function HierarchyTab() {
|
|||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-destructive hover:bg-destructive">
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ export default function MutualExclusionTab() {
|
|||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<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>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -404,7 +404,7 @@ export default function MutualExclusionTab() {
|
|||
/>
|
||||
{fieldList.length > 2 && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -571,7 +571,7 @@ export default function MutualExclusionTab() {
|
|||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-red-500 hover:bg-red-600">
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-destructive hover:bg-destructive">
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export default function DebugLayoutPage() {
|
|||
<h1 className="mb-4 text-2xl font-bold">관리자 레이아웃 디버깅</h1>
|
||||
|
||||
<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>
|
||||
<p>토큰 존재: {debugInfo.hasToken ? "✅ 예" : "❌ 아니오"}</p>
|
||||
<p>토큰 길이: {debugInfo.tokenLength}</p>
|
||||
|
|
@ -142,14 +142,14 @@ export default function DebugLayoutPage() {
|
|||
<p>SessionStorage 토큰: {debugInfo.sessionToken ? "✅ 존재" : "❌ 없음"}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded bg-blue-100 p-4">
|
||||
<div className="rounded bg-primary/10 p-4">
|
||||
<h2 className="mb-2 font-semibold">페이지 정보</h2>
|
||||
<p>현재 URL: {debugInfo.currentUrl}</p>
|
||||
<p>Pathname: {debugInfo.pathname}</p>
|
||||
<p>시간: {debugInfo.timestamp}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded bg-yellow-100 p-4">
|
||||
<div className="rounded bg-amber-100 p-4">
|
||||
<h2 className="mb-2 font-semibold">토큰 관리</h2>
|
||||
<div className="space-x-2">
|
||||
<button
|
||||
|
|
@ -157,11 +157,11 @@ export default function DebugLayoutPage() {
|
|||
const token = localStorage.getItem("authToken");
|
||||
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 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
|
||||
|
|
@ -173,13 +173,13 @@ export default function DebugLayoutPage() {
|
|||
</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>
|
||||
<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 테스트
|
||||
</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 테스트
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -194,7 +194,7 @@ export default function DebugLayoutPage() {
|
|||
<div
|
||||
className={`rounded p-3 ${
|
||||
apiTestResult.status === "success"
|
||||
? "bg-green-200"
|
||||
? "bg-emerald-200"
|
||||
: apiTestResult.status === "error"
|
||||
? "bg-red-200"
|
||||
: "bg-yellow-200"
|
||||
|
|
|
|||
|
|
@ -28,27 +28,27 @@ export default function SimpleDebugPage() {
|
|||
<h1 className="mb-4 text-2xl font-bold">간단한 토큰 디버깅</h1>
|
||||
|
||||
<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>
|
||||
<p>토큰 존재: {tokenInfo.hasToken ? "✅ 예" : "❌ 아니오"}</p>
|
||||
<p>토큰 길이: {tokenInfo.tokenLength}</p>
|
||||
<p>토큰 시작: {tokenInfo.tokenStart}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded bg-blue-100 p-4">
|
||||
<div className="rounded bg-primary/10 p-4">
|
||||
<h2 className="mb-2 font-semibold">페이지 정보</h2>
|
||||
<p>현재 URL: {tokenInfo.currentUrl}</p>
|
||||
<p>시간: {tokenInfo.timestamp}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded bg-yellow-100 p-4">
|
||||
<div className="rounded bg-amber-100 p-4">
|
||||
<h2 className="mb-2 font-semibold">테스트 버튼</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
const token = localStorage.getItem("authToken");
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ export default function AdminDebugPage() {
|
|||
<h1 className="mb-4 text-2xl font-bold">어드민 권한 디버깅</h1>
|
||||
|
||||
<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>
|
||||
<p>로딩: {loading ? "예" : "아니오"}</p>
|
||||
<p>로그인: {isLoggedIn ? "예" : "아니오"}</p>
|
||||
<p>관리자: {isAdmin ? "예" : "아니오"}</p>
|
||||
{error && <p className="text-red-500">에러: {error}</p>}
|
||||
{error && <p className="text-destructive">에러: {error}</p>}
|
||||
</div>
|
||||
|
||||
{user && (
|
||||
<div className="rounded bg-blue-100 p-4">
|
||||
<div className="rounded bg-primary/10 p-4">
|
||||
<h2 className="mb-2 font-semibold">사용자 정보</h2>
|
||||
<p>ID: {user.userId}</p>
|
||||
<p>이름: {user.userName}</p>
|
||||
|
|
@ -34,7 +34,7 @@ export default function AdminDebugPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded bg-green-100 p-4">
|
||||
<div className="rounded bg-emerald-100 p-4">
|
||||
<h2 className="mb-2 font-semibold">토큰 정보</h2>
|
||||
<p>
|
||||
localStorage 토큰: {typeof window !== "undefined" && localStorage.getItem("authToken") ? "존재" : "없음"}
|
||||
|
|
|
|||
|
|
@ -220,13 +220,13 @@ export default function LayoutManagementPage() {
|
|||
};
|
||||
|
||||
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="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">레이아웃 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면 레이아웃을 생성하고 관리합니다</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">레이아웃 관리</h1>
|
||||
<p className="mt-2 text-muted-foreground">화면 레이아웃을 생성하고 관리합니다</p>
|
||||
</div>
|
||||
<Button className="flex items-center gap-2 shadow-sm" onClick={() => setCreateModalOpen(true)}>
|
||||
<Plus className="h-4 w-4" />새 레이아웃
|
||||
|
|
@ -239,7 +239,7 @@ export default function LayoutManagementPage() {
|
|||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<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
|
||||
placeholder="레이아웃 이름 또는 설명으로 검색..."
|
||||
value={searchTerm}
|
||||
|
|
@ -276,7 +276,7 @@ export default function LayoutManagementPage() {
|
|||
{loading ? (
|
||||
<div className="py-8 text-center">로딩 중...</div>
|
||||
) : 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">
|
||||
<div className="flex items-start justify-between">
|
||||
<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">
|
||||
{CATEGORY_NAMES[layout.category as keyof typeof CATEGORY_NAMES]}
|
||||
</Badge>
|
||||
|
|
@ -312,7 +312,7 @@ export default function LayoutManagementPage() {
|
|||
<Copy className="mr-2 h-4 w-4" />
|
||||
복제
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(layout)} className="text-red-600">
|
||||
<DropdownMenuItem onClick={() => handleDelete(layout)} className="text-destructive">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
삭제
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -321,17 +321,17 @@ export default function LayoutManagementPage() {
|
|||
</div>
|
||||
<CardTitle className="text-lg">{layout.layoutName}</CardTitle>
|
||||
{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>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
{layout.isPublic === "Y" && (
|
||||
|
|
@ -397,7 +397,7 @@ export default function LayoutManagementPage() {
|
|||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmDelete} className="bg-red-600 hover:bg-red-700">
|
||||
<AlertDialogAction onClick={confirmDelete} className="bg-destructive hover:bg-red-700">
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -59,25 +59,25 @@ export default function MonitoringPage() {
|
|||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||
return <CheckCircle className="h-4 w-4 text-emerald-500" />;
|
||||
case 'failed':
|
||||
return <AlertCircle className="h-4 w-4 text-red-500" />;
|
||||
return <AlertCircle className="h-4 w-4 text-destructive" />;
|
||||
case 'running':
|
||||
return <Play className="h-4 w-4 text-blue-500" />;
|
||||
return <Play className="h-4 w-4 text-primary" />;
|
||||
case 'pending':
|
||||
return <Clock className="h-4 w-4 text-yellow-500" />;
|
||||
return <Clock className="h-4 w-4 text-amber-500" />;
|
||||
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 variants = {
|
||||
completed: "bg-green-100 text-green-800",
|
||||
completed: "bg-emerald-100 text-emerald-800",
|
||||
failed: "bg-destructive/20 text-red-800",
|
||||
running: "bg-primary/20 text-blue-800",
|
||||
pending: "bg-yellow-100 text-yellow-800",
|
||||
cancelled: "bg-gray-100 text-gray-800",
|
||||
running: "bg-primary/20 text-primary",
|
||||
pending: "bg-amber-100 text-yellow-800",
|
||||
cancelled: "bg-muted text-foreground",
|
||||
};
|
||||
|
||||
const labels = {
|
||||
|
|
@ -120,7 +120,7 @@ export default function MonitoringPage() {
|
|||
}
|
||||
|
||||
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>
|
||||
|
|
@ -191,7 +191,7 @@ export default function MonitoringPage() {
|
|||
<div className="text-2xl">✅</div>
|
||||
</CardHeader>
|
||||
<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">
|
||||
성공률: {getSuccessRate()}%
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default function BarcodeLabelDesignerPage() {
|
|||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<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 />
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<BarcodeDesignerLeftPanel />
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ export default function BarcodeLabelManagementPage() {
|
|||
};
|
||||
|
||||
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="flex items-center justify-between rounded-lg border bg-white p-6 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">바코드 라벨 관리</h1>
|
||||
<p className="mt-2 text-gray-600">ZD421 등 바코드 프린터용 라벨을 작성하고 출력합니다</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">바코드 라벨 관리</h1>
|
||||
<p className="mt-2 text-muted-foreground">ZD421 등 바코드 프린터용 라벨을 작성하고 출력합니다</p>
|
||||
</div>
|
||||
<Button onClick={handleCreateNew} className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
|
|
@ -43,7 +43,7 @@ export default function BarcodeLabelManagementPage() {
|
|||
</div>
|
||||
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardHeader className="bg-muted/50">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Search className="h-5 w-5" />
|
||||
검색
|
||||
|
|
@ -75,7 +75,7 @@ export default function BarcodeLabelManagementPage() {
|
|||
</Card>
|
||||
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardHeader className="bg-muted/50">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
바코드 라벨 목록
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export default function ReportDesignerPage() {
|
|||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<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 />
|
||||
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ export default function ReportManagementPage() {
|
|||
};
|
||||
|
||||
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="flex items-center justify-between rounded-lg border bg-white p-6 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">리포트 관리</h1>
|
||||
<p className="mt-2 text-gray-600">리포트를 생성하고 관리합니다</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">리포트 관리</h1>
|
||||
<p className="mt-2 text-muted-foreground">리포트를 생성하고 관리합니다</p>
|
||||
</div>
|
||||
<Button onClick={handleCreateNew} className="gap-2">
|
||||
<Plus className="h-4 w-4" />새 리포트
|
||||
|
|
@ -45,7 +45,7 @@ export default function ReportManagementPage() {
|
|||
|
||||
{/* 검색 영역 */}
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardHeader className="bg-muted/50">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Search className="h-5 w-5" />
|
||||
검색
|
||||
|
|
@ -78,7 +78,7 @@ export default function ReportManagementPage() {
|
|||
|
||||
{/* 리포트 목록 */}
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardHeader className="bg-muted/50">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
📋 리포트 목록
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ export default function EditWebTypePage() {
|
|||
}
|
||||
|
||||
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="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="space-y-2">
|
||||
<Label htmlFor="type_name">
|
||||
웹타입명 <span className="text-red-500">*</span>
|
||||
웹타입명 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="type_name"
|
||||
|
|
@ -267,7 +267,7 @@ export default function EditWebTypePage() {
|
|||
{/* 카테고리 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">
|
||||
카테고리 <span className="text-red-500">*</span>
|
||||
카테고리 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select value={formData.category || ""} onValueChange={(value) => handleInputChange("category", value)}>
|
||||
<SelectTrigger>
|
||||
|
|
@ -286,7 +286,7 @@ export default function EditWebTypePage() {
|
|||
{/* 연결된 컴포넌트 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="component_name">
|
||||
연결된 컴포넌트 <span className="text-red-500">*</span>
|
||||
연결된 컴포넌트 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.component_name || "TextWidget"}
|
||||
|
|
@ -338,15 +338,15 @@ export default function EditWebTypePage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
{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="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm font-medium text-green-700">
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-500"></div>
|
||||
<span className="text-sm font-medium text-emerald-700">
|
||||
현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel}
|
||||
</span>
|
||||
</div>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -427,7 +427,7 @@ export default function EditWebTypePage() {
|
|||
rows={4}
|
||||
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>
|
||||
|
||||
{/* 유효성 검사 규칙 */}
|
||||
|
|
@ -441,7 +441,7 @@ export default function EditWebTypePage() {
|
|||
rows={4}
|
||||
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>
|
||||
|
||||
{/* 기본 스타일 */}
|
||||
|
|
@ -455,7 +455,7 @@ export default function EditWebTypePage() {
|
|||
rows={4}
|
||||
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>
|
||||
|
||||
{/* 입력 속성 */}
|
||||
|
|
@ -469,7 +469,7 @@ export default function EditWebTypePage() {
|
|||
rows={4}
|
||||
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>
|
||||
</CardContent>
|
||||
|
|
@ -498,8 +498,8 @@ export default function EditWebTypePage() {
|
|||
|
||||
{/* 에러 메시지 */}
|
||||
{updateError && (
|
||||
<div className="mt-4 rounded-md border border-red-200 bg-red-50 p-4">
|
||||
<p className="text-red-600">
|
||||
<div className="mt-4 rounded-md border border-destructive/20 bg-destructive/10 p-4">
|
||||
<p className="text-destructive">
|
||||
수정 중 오류가 발생했습니다: {updateError instanceof Error ? updateError.message : "알 수 없는 오류"}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export default function WebTypeDetailPage() {
|
|||
return (
|
||||
<div className="flex h-96 items-center justify-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">
|
||||
<Button variant="outline">목록으로 돌아가기</Button>
|
||||
</Link>
|
||||
|
|
@ -80,7 +80,7 @@ export default function WebTypeDetailPage() {
|
|||
}
|
||||
|
||||
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="mb-6 flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ export default function NewWebTypePage() {
|
|||
};
|
||||
|
||||
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="mb-6 flex items-center gap-4">
|
||||
|
|
@ -186,7 +186,7 @@ export default function NewWebTypePage() {
|
|||
{/* 웹타입 코드 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="web_type">
|
||||
웹타입 코드 <span className="text-red-500">*</span>
|
||||
웹타입 코드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
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="space-y-2">
|
||||
<Label htmlFor="type_name">
|
||||
웹타입명 <span className="text-red-500">*</span>
|
||||
웹타입명 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="type_name"
|
||||
|
|
@ -225,7 +225,7 @@ export default function NewWebTypePage() {
|
|||
{/* 카테고리 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">
|
||||
카테고리 <span className="text-red-500">*</span>
|
||||
카테고리 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select value={formData.category} onValueChange={(value) => handleInputChange("category", value)}>
|
||||
<SelectTrigger>
|
||||
|
|
@ -244,7 +244,7 @@ export default function NewWebTypePage() {
|
|||
{/* 연결된 컴포넌트 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="component_name">
|
||||
연결된 컴포넌트 <span className="text-red-500">*</span>
|
||||
연결된 컴포넌트 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.component_name || "TextWidget"}
|
||||
|
|
@ -296,15 +296,15 @@ export default function NewWebTypePage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
{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="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm font-medium text-green-700">
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-500"></div>
|
||||
<span className="text-sm font-medium text-emerald-700">
|
||||
현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel}
|
||||
</span>
|
||||
</div>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -385,7 +385,7 @@ export default function NewWebTypePage() {
|
|||
rows={4}
|
||||
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>
|
||||
|
||||
{/* 유효성 검사 규칙 */}
|
||||
|
|
@ -399,7 +399,7 @@ export default function NewWebTypePage() {
|
|||
rows={4}
|
||||
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>
|
||||
|
||||
{/* 기본 스타일 */}
|
||||
|
|
@ -413,7 +413,7 @@ export default function NewWebTypePage() {
|
|||
rows={4}
|
||||
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>
|
||||
|
||||
{/* 입력 속성 */}
|
||||
|
|
@ -427,7 +427,7 @@ export default function NewWebTypePage() {
|
|||
rows={4}
|
||||
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>
|
||||
</CardContent>
|
||||
|
|
@ -448,8 +448,8 @@ export default function NewWebTypePage() {
|
|||
|
||||
{/* 에러 메시지 */}
|
||||
{createError && (
|
||||
<div className="mt-4 rounded-md border border-red-200 bg-red-50 p-4">
|
||||
<p className="text-red-600">
|
||||
<div className="mt-4 rounded-md border border-destructive/20 bg-destructive/10 p-4">
|
||||
<p className="text-destructive">
|
||||
생성 중 오류가 발생했습니다: {createError instanceof Error ? createError.message : "알 수 없는 오류"}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -142,28 +142,28 @@ export default function CollectionManagementPage() {
|
|||
|
||||
const getStatusBadge = (isActive: string) => {
|
||||
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 option = collectionTypeOptions.find(opt => opt.value === type);
|
||||
const colors = {
|
||||
full: "bg-blue-100 text-blue-800",
|
||||
full: "bg-primary/10 text-primary",
|
||||
incremental: "bg-purple-100 text-purple-800",
|
||||
delta: "bg-orange-100 text-orange-800",
|
||||
delta: "bg-amber-100 text-orange-800",
|
||||
};
|
||||
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}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
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="flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ export default function DataFlowEditPage() {
|
|||
return (
|
||||
<div className="flex h-64 items-center justify-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>
|
||||
<p className="text-gray-500">관계도 정보를 불러오는 중...</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -68,16 +68,16 @@ export default function DataFlowEditPage() {
|
|||
<span>목록으로</span>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">📊 관계도 편집</h1>
|
||||
<p className="mt-1 text-gray-600">
|
||||
<span className="font-medium text-blue-600">{diagramName}</span> 관계도를 편집하고 있습니다
|
||||
<h1 className="text-2xl font-bold text-foreground">📊 관계도 편집</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
<span className="font-medium text-primary">{diagramName}</span> 관계도를 편집하고 있습니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데이터플로우 디자이너 */}
|
||||
<div className="rounded-lg border border-gray-200 bg-white">
|
||||
<div className="rounded-lg border border-border bg-white">
|
||||
<DataFlowDesigner
|
||||
key={diagramId}
|
||||
selectedDiagram={diagramName}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export default function NodeEditorPage() {
|
|||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-gray-50">
|
||||
<div className="text-gray-500">제어 관리 페이지로 이동중...</div>
|
||||
<div className="flex h-screen items-center justify-center bg-muted">
|
||||
<div className="text-muted-foreground">제어 관리 페이지로 이동중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -528,14 +528,14 @@ export default function I18nPage() {
|
|||
? "공통"
|
||||
: 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",
|
||||
header: "메뉴명",
|
||||
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: "언어 키",
|
||||
cell: ({ row }: any) => (
|
||||
<div
|
||||
className={`cursor-pointer rounded p-1 hover:bg-gray-100 ${
|
||||
row.original.isActive === "N" ? "text-gray-400" : ""
|
||||
className={`cursor-pointer rounded p-1 hover:bg-muted ${
|
||||
row.original.isActive === "N" ? "text-muted-foreground/70" : ""
|
||||
}`}
|
||||
onDoubleClick={() => handleEditKey(row.original)}
|
||||
>
|
||||
|
|
@ -556,7 +556,7 @@ export default function I18nPage() {
|
|||
accessorKey: "description",
|
||||
header: "설명",
|
||||
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)}
|
||||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
row.original.isActive === "Y"
|
||||
? "bg-green-100 text-green-800 hover:bg-green-200"
|
||||
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
|
||||
? "bg-emerald-100 text-emerald-800 hover:bg-emerald-200"
|
||||
: "bg-muted text-foreground hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
{row.original.isActive === "Y" ? "활성" : "비활성"}
|
||||
|
|
@ -605,8 +605,8 @@ export default function I18nPage() {
|
|||
header: "언어 코드",
|
||||
cell: ({ row }: any) => (
|
||||
<div
|
||||
className={`cursor-pointer rounded p-1 hover:bg-gray-100 ${
|
||||
row.original.isActive === "N" ? "text-gray-400" : ""
|
||||
className={`cursor-pointer rounded p-1 hover:bg-muted ${
|
||||
row.original.isActive === "N" ? "text-muted-foreground/70" : ""
|
||||
}`}
|
||||
onDoubleClick={() => handleEditLanguage(row.original)}
|
||||
>
|
||||
|
|
@ -618,14 +618,14 @@ export default function I18nPage() {
|
|||
accessorKey: "langName",
|
||||
header: "언어명 (영문)",
|
||||
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",
|
||||
header: "언어명 (원어)",
|
||||
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)}
|
||||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
row.original.isActive === "Y"
|
||||
? "bg-green-100 text-green-800 hover:bg-green-200"
|
||||
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
|
||||
? "bg-emerald-100 text-emerald-800 hover:bg-emerald-200"
|
||||
: "bg-muted text-foreground hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
{row.original.isActive === "Y" ? "활성" : "비활성"}
|
||||
|
|
@ -651,7 +651,7 @@ export default function I18nPage() {
|
|||
}
|
||||
|
||||
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="container mx-auto p-2">
|
||||
{/* 탭 네비게이션 */}
|
||||
|
|
@ -659,7 +659,7 @@ export default function I18nPage() {
|
|||
<button
|
||||
onClick={() => setActiveTab("keys")}
|
||||
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
|
||||
onClick={() => setActiveTab("languages")}
|
||||
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 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="mb-2 text-lg font-medium">언어 키를 선택하세요</div>
|
||||
<div className="text-sm">좌측 목록에서 편집할 언어 키를 클릭하세요</div>
|
||||
|
|
|
|||
|
|
@ -120,12 +120,12 @@ export default function TemplatesManagePage() {
|
|||
|
||||
// 간단한 아이콘 매핑 (실제로는 더 복잡한 시스템 필요)
|
||||
const iconMap: Record<string, JSX.Element> = {
|
||||
table: <div className="h-4 w-4 border border-gray-400" />,
|
||||
"mouse-pointer": <div className="h-4 w-4 rounded bg-blue-500" />,
|
||||
upload: <div className="h-4 w-4 border-2 border-dashed border-gray-400" />,
|
||||
table: <div className="h-4 w-4 border border-input" />,
|
||||
"mouse-pointer": <div className="h-4 w-4 rounded bg-primary" />,
|
||||
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) {
|
||||
|
|
@ -133,7 +133,7 @@ export default function TemplatesManagePage() {
|
|||
<div className="w-full max-w-none px-4 py-8">
|
||||
<Card>
|
||||
<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">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
다시 시도
|
||||
|
|
@ -145,13 +145,13 @@ export default function TemplatesManagePage() {
|
|||
}
|
||||
|
||||
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="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">템플릿 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면 디자이너에서 사용할 템플릿을 관리합니다</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">템플릿 관리</h1>
|
||||
<p className="mt-2 text-muted-foreground">화면 디자이너에서 사용할 템플릿을 관리합니다</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button asChild className="shadow-sm">
|
||||
|
|
@ -164,9 +164,9 @@ export default function TemplatesManagePage() {
|
|||
|
||||
{/* 필터 및 검색 */}
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardHeader className="bg-muted/50">
|
||||
<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>
|
||||
</CardHeader>
|
||||
|
|
@ -176,7 +176,7 @@ export default function TemplatesManagePage() {
|
|||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">검색</label>
|
||||
<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
|
||||
placeholder="템플릿명, 설명 검색..."
|
||||
value={searchTerm}
|
||||
|
|
@ -232,7 +232,7 @@ export default function TemplatesManagePage() {
|
|||
|
||||
{/* 템플릿 목록 테이블 */}
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader className="bg-gray-50/50">
|
||||
<CardHeader className="bg-muted/50">
|
||||
<CardTitle>템플릿 목록 ({filteredAndSortedTemplates.length}개)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
@ -293,7 +293,7 @@ export default function TemplatesManagePage() {
|
|||
</TableRow>
|
||||
) : filteredAndSortedTemplates.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={11} className="py-8 text-center text-gray-500">
|
||||
<TableCell colSpan={11} className="py-8 text-center text-muted-foreground">
|
||||
검색 조건에 맞는 템플릿이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -357,7 +357,7 @@ export default function TemplatesManagePage() {
|
|||
</Button>
|
||||
<AlertDialog>
|
||||
<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" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
|
@ -373,7 +373,7 @@ export default function TemplatesManagePage() {
|
|||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
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}
|
||||
>
|
||||
삭제
|
||||
|
|
|
|||
|
|
@ -25,27 +25,27 @@ export default function TestPage() {
|
|||
<h1 className="mb-4 text-2xl font-bold">토큰 테스트 페이지</h1>
|
||||
|
||||
<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>
|
||||
<p>토큰 존재: {tokenInfo.hasToken ? "✅ 예" : "❌ 아니오"}</p>
|
||||
<p>토큰 길이: {tokenInfo.tokenLength}</p>
|
||||
<p>토큰 시작: {tokenInfo.tokenStart}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded bg-blue-100 p-4">
|
||||
<div className="rounded bg-primary/10 p-4">
|
||||
<h2 className="mb-2 font-semibold">페이지 정보</h2>
|
||||
<p>현재 URL: {tokenInfo.currentUrl}</p>
|
||||
<p>시간: {tokenInfo.timestamp}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded bg-yellow-100 p-4">
|
||||
<div className="rounded bg-amber-100 p-4">
|
||||
<h2 className="mb-2 font-semibold">테스트 버튼</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
const token = localStorage.getItem("authToken");
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ export default function TokenTestPage() {
|
|||
<h1 className="mb-4 text-2xl font-bold">토큰 상태 테스트</h1>
|
||||
|
||||
<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>
|
||||
<pre className="text-sm">{JSON.stringify(tokenInfo, null, 2)}</pre>
|
||||
</div>
|
||||
|
||||
<div className="rounded bg-blue-100 p-4">
|
||||
<div className="rounded bg-primary/10 p-4">
|
||||
<h2 className="mb-2 font-semibold">테스트 버튼</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -44,7 +44,7 @@ export default function TokenTestPage() {
|
|||
console.log("현재 토큰:", 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>
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ export default function RoleDetailPage({ params }: { params: Promise<{ id: strin
|
|||
</div>
|
||||
<span
|
||||
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" ? "활성" : "비활성"}
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ export default function RolesPage() {
|
|||
</div>
|
||||
<span
|
||||
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" ? "활성" : "비활성"}
|
||||
|
|
|
|||
|
|
@ -388,7 +388,7 @@ export default function ValidationDemoPage() {
|
|||
<CardDescription>실시간 검증이 적용된 폼입니다. 입력하면서 검증 결과를 확인해보세요.</CardDescription>
|
||||
</CardHeader>
|
||||
<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
|
||||
component={TEST_COMPONENTS[0]} // container
|
||||
allComponents={TEST_COMPONENTS}
|
||||
|
|
@ -485,7 +485,7 @@ export default function ValidationDemoPage() {
|
|||
|
||||
<div className="space-y-2">
|
||||
<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)}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -493,15 +493,15 @@ export default function ValidationDemoPage() {
|
|||
<div className="space-y-2">
|
||||
<h4 className="font-semibold">검증 통계</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded-md bg-green-50 p-3">
|
||||
<div className="text-lg font-bold text-green-600">
|
||||
<div className="rounded-md bg-emerald-50 p-3">
|
||||
<div className="text-lg font-bold text-emerald-600">
|
||||
{Object.values(validationState.fieldStates).filter((f) => f.status === "valid").length}
|
||||
</div>
|
||||
<div className="text-sm text-green-700">유효한 필드</div>
|
||||
<div className="text-sm text-emerald-700">유효한 필드</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-red-50 p-3">
|
||||
<div className="text-lg font-bold text-red-600">{validationState.errors.length}</div>
|
||||
<div className="text-sm text-red-700">오류 개수</div>
|
||||
<div className="rounded-md bg-destructive/10 p-3">
|
||||
<div className="text-lg font-bold text-destructive">{validationState.errors.length}</div>
|
||||
<div className="text-sm text-destructive">오류 개수</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 }> = {
|
||||
waiting: { label: "대기", icon: <Clock className="h-3 w-3 text-muted-foreground" /> },
|
||||
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" /> },
|
||||
skipped: { label: "건너뜀", icon: <Clock className="h-3 w-3 text-muted-foreground" /> },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -107,18 +107,18 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
|||
return (
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold text-gray-800">{dashboard.title}</h1>
|
||||
{dashboard.description && <p className="mt-1 text-sm text-gray-600">{dashboard.description}</p>}
|
||||
<h1 className="text-2xl font-bold text-foreground">{dashboard.title}</h1>
|
||||
{dashboard.description && <p className="mt-1 text-sm text-muted-foreground">{dashboard.description}</p>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 새로고침 버튼 *\/}
|
||||
<button
|
||||
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="새로고침"
|
||||
>
|
||||
🔄
|
||||
|
|
@ -133,7 +133,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
|||
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="전체화면"
|
||||
>
|
||||
⛶
|
||||
|
|
@ -144,7 +144,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
|||
onClick={() => {
|
||||
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>
|
||||
|
|
@ -152,7 +152,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
|||
</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.updatedAt).toLocaleString()}</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";
|
||||
|
||||
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: 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" },
|
||||
|
|
|
|||
|
|
@ -385,13 +385,13 @@ export default function MultiLangPage() {
|
|||
</div>
|
||||
|
||||
<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 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>
|
||||
|
||||
<DataTable
|
||||
|
|
@ -417,7 +417,7 @@ export default function MultiLangPage() {
|
|||
{languages.map((lang) => (
|
||||
<div key={lang.langCode} className="rounded-lg border p-4">
|
||||
<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">
|
||||
{lang.isActive === "Y" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
|
|
@ -440,7 +440,7 @@ export default function MultiLangPage() {
|
|||
<h3 className="mb-2 font-semibold">{company.name}</h3>
|
||||
<div className="space-y-1">
|
||||
{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}
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
|||
import { FileCheck, Menu, Users, Bell, FileText, Layout, Server, Shield, Calendar, ArrowRight } from "lucide-react";
|
||||
|
||||
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: 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" },
|
||||
|
|
|
|||
|
|
@ -174,10 +174,10 @@ function PopScreenViewPage() {
|
|||
|
||||
if (loading) {
|
||||
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">
|
||||
<Loader2 className="mx-auto h-10 w-10 animate-spin text-blue-500" />
|
||||
<p className="mt-4 text-gray-600">POP 화면 로딩 중...</p>
|
||||
<Loader2 className="mx-auto h-10 w-10 animate-spin text-primary" />
|
||||
<p className="mt-4 text-muted-foreground">POP 화면 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -185,13 +185,13 @@ function PopScreenViewPage() {
|
|||
|
||||
if (error || !screen) {
|
||||
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="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>
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-bold text-gray-800">화면을 찾을 수 없습니다</h2>
|
||||
<p className="mb-4 text-gray-600">{error || "요청하신 POP 화면이 존재하지 않습니다."}</p>
|
||||
<h2 className="mb-2 text-xl font-bold text-foreground">화면을 찾을 수 없습니다</h2>
|
||||
<p className="mb-4 text-muted-foreground">{error || "요청하신 POP 화면이 존재하지 않습니다."}</p>
|
||||
<Button onClick={() => router.back()} variant="outline">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
돌아가기
|
||||
|
|
@ -205,7 +205,7 @@ function PopScreenViewPage() {
|
|||
<ScreenPreviewProvider isPreviewMode={isPreviewMode}>
|
||||
<ActiveTabProvider>
|
||||
<TableOptionsProvider>
|
||||
<div className="h-screen bg-gray-100 flex flex-col">
|
||||
<div className="h-screen bg-muted flex flex-col">
|
||||
{/* 상단 툴바 (프리뷰 모드에서만) */}
|
||||
{isPreviewMode && (
|
||||
<div className="sticky top-0 z-50 bg-white border-b shadow-sm">
|
||||
|
|
@ -216,13 +216,13 @@ function PopScreenViewPage() {
|
|||
닫기
|
||||
</Button>
|
||||
<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("_", " ")})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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
|
||||
variant={deviceType === "mobile" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
|
|
@ -243,7 +243,7 @@ function PopScreenViewPage() {
|
|||
</Button>
|
||||
</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
|
||||
variant={isLandscape ? "default" : "ghost"}
|
||||
size="sm"
|
||||
|
|
@ -295,7 +295,7 @@ function PopScreenViewPage() {
|
|||
)}
|
||||
|
||||
<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 ? {
|
||||
width: currentDevice.width,
|
||||
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="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
||||
<Smartphone className="h-8 w-8 text-gray-400" />
|
||||
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
||||
<Smartphone className="h-8 w-8 text-muted-foreground/70" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||
화면이 비어있습니다
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 max-w-xs">
|
||||
<p className="text-sm text-muted-foreground max-w-xs">
|
||||
POP 화면 디자이너에서 컴포넌트를 추가하여 화면을 구성하세요.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -227,9 +227,9 @@ export default function SimpleTypeSafetyTest() {
|
|||
</div>
|
||||
|
||||
{passedTests === totalTests && totalTests > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-green-200 bg-green-50 p-4">
|
||||
<div className="font-medium text-green-800">🎉 모든 타입 안전성 테스트가 통과되었습니다!</div>
|
||||
<div className="mt-2 text-sm text-green-600">
|
||||
<div className="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 p-4">
|
||||
<div className="font-medium text-emerald-800">🎉 모든 타입 안전성 테스트가 통과되었습니다!</div>
|
||||
<div className="mt-2 text-sm text-emerald-600">
|
||||
화면관리, 제어관리, 테이블타입관리 시스템이 안전하게 작동합니다.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export default function StressTestPage() {
|
|||
<div className="space-y-4 text-center">
|
||||
<h1 className="text-3xl font-bold">🔥 타입 시스템 스트레스 테스트</h1>
|
||||
<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>
|
||||
|
||||
|
|
@ -184,19 +184,19 @@ export default function StressTestPage() {
|
|||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
|
@ -212,16 +212,16 @@ export default function StressTestPage() {
|
|||
</div>
|
||||
|
||||
{testResults.success ? (
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 p-4">
|
||||
<div className="font-medium text-green-800">🎉 모든 스트레스 테스트 통과!</div>
|
||||
<div className="mt-2 text-sm text-green-600">
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-4">
|
||||
<div className="font-medium text-emerald-800">🎉 모든 스트레스 테스트 통과!</div>
|
||||
<div className="mt-2 text-sm text-emerald-600">
|
||||
타입 시스템이 극한 상황에서도 안정적으로 작동합니다.
|
||||
</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="mt-2 text-sm text-red-600">
|
||||
<div className="mt-2 text-sm text-destructive">
|
||||
개선이 필요한 영역이 있습니다. 아래 권장사항을 확인하세요.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -253,7 +253,7 @@ export default function StressTestPage() {
|
|||
|
||||
{/* 메트릭스 표시 */}
|
||||
{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="grid grid-cols-2 gap-2">
|
||||
{Object.entries(result.metrics).map(([key, value]) => (
|
||||
|
|
@ -286,7 +286,7 @@ export default function StressTestPage() {
|
|||
<ul className="space-y-2">
|
||||
{testResults.recommendation.map((rec, index) => (
|
||||
<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>
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -304,7 +304,7 @@ export default function StressTestPage() {
|
|||
<CardTitle className="text-lg">📋 테스트 로그 (최근 10개)</CardTitle>
|
||||
</CardHeader>
|
||||
<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) => (
|
||||
<div key={index}>{log}</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -66,10 +66,10 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
|||
return <Video {...iconProps} className="text-purple-600" />;
|
||||
}
|
||||
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)) {
|
||||
return <Archive {...iconProps} className="text-yellow-600" />;
|
||||
return <Archive {...iconProps} className="text-amber-600" />;
|
||||
}
|
||||
if (['txt', 'md', 'doc', 'docx', 'pdf', 'rtf'].includes(extension)) {
|
||||
return <FileText {...iconProps} className="text-destructive" />;
|
||||
|
|
@ -192,7 +192,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
|||
{showControls && (
|
||||
<div className="flex items-center gap-2">
|
||||
<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
|
||||
placeholder="파일명으로 검색..."
|
||||
value={searchQuery}
|
||||
|
|
@ -219,7 +219,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
|||
style={{ maxHeight }}
|
||||
>
|
||||
{filteredFiles.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{searchQuery ? "검색 결과가 없습니다." : "저장된 파일이 없습니다."}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -232,7 +232,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
|||
<div className="font-medium truncate">
|
||||
{file.realFileName || file.savedFileName}
|
||||
</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>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
|
|
@ -273,7 +273,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
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" />
|
||||
</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="space-y-2">
|
||||
<Label htmlFor="columnName">
|
||||
컬럼명 <span className="text-red-500">*</span>
|
||||
컬럼명 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="columnName"
|
||||
|
|
@ -233,7 +233,7 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
|||
onChange={(e) => updateColumn({ name: e.target.value })}
|
||||
placeholder="column_name"
|
||||
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>
|
||||
</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="space-y-2">
|
||||
<Label>
|
||||
입력타입 <span className="text-red-500">*</span>
|
||||
입력타입 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select value={column.inputType} onValueChange={handleInputTypeChange} disabled={loading}>
|
||||
<SelectTrigger>
|
||||
|
|
@ -354,7 +354,7 @@ export function AddColumnModal({ isOpen, onClose, tableName, onSuccess }: AddCol
|
|||
<Button
|
||||
onClick={handleAddColumn}
|
||||
disabled={!isFormValid || loading}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
className="bg-primary hover:bg-primary/90"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export function AuthenticationConfig({
|
|||
|
||||
{/* 인증 타입별 설정 필드 */}
|
||||
{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>
|
||||
|
||||
{/* 키 위치 */}
|
||||
|
|
@ -96,7 +96,7 @@ export function AuthenticationConfig({
|
|||
)}
|
||||
|
||||
{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>
|
||||
|
||||
{/* 토큰 */}
|
||||
|
|
@ -111,14 +111,14 @@ export function AuthenticationConfig({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
* Authorization 헤더에 "Bearer {token}" 형식으로 전송됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
|
||||
{/* 사용자명 */}
|
||||
|
|
@ -145,12 +145,12 @@ export function AuthenticationConfig({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500">* Authorization 헤더에 Base64 인코딩된 인증 정보가 전송됩니다.</p>
|
||||
<p className="text-xs text-muted-foreground">* Authorization 헤더에 Base64 인코딩된 인증 정보가 전송됩니다.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
|
||||
{/* Client ID */}
|
||||
|
|
@ -189,12 +189,12 @@ export function AuthenticationConfig({
|
|||
/>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
|
@ -275,14 +275,14 @@ export function AuthenticationConfig({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
company_code는 현재 로그인한 사용자의 회사 코드로 자동 필터링됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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입니다.
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -341,14 +341,14 @@ export function CreateTableModal({
|
|||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tableName">
|
||||
테이블명 <span className="text-red-500">*</span>
|
||||
테이블명 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="tableName"
|
||||
value={tableName}
|
||||
onChange={(e) => handleTableNameChange(e.target.value)}
|
||||
placeholder="예: customer_info"
|
||||
className={tableNameError ? "border-red-300" : ""}
|
||||
className={tableNameError ? "border-destructive/30" : ""}
|
||||
/>
|
||||
{tableNameError && <p className="text-destructive text-sm">{tableNameError}</p>}
|
||||
<p className="text-muted-foreground text-xs">영문자로 시작, 영문자/숫자/언더스코어만 사용 가능</p>
|
||||
|
|
@ -369,7 +369,7 @@ export function CreateTableModal({
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>
|
||||
컬럼 정의 <span className="text-red-500">*</span>
|
||||
컬럼 정의 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Button type="button" variant="outline" size="sm" onClick={addColumn} disabled={loading}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
|
|
@ -440,7 +440,7 @@ export function CreateTableModal({
|
|||
<div className="font-medium">경고:</div>
|
||||
<ul className="list-inside list-disc space-y-1">
|
||||
{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}
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -471,7 +471,7 @@ export function CreateTableModal({
|
|||
<Button
|
||||
onClick={handleCreateTable}
|
||||
disabled={!isFormValid || loading || (validationResult && !validationResult.isValid)}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
className="bg-emerald-600 hover:bg-green-700"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -276,11 +276,11 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
|
|||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{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" />
|
||||
)}
|
||||
<span className={log.success ? "text-green-600" : "text-destructive"}>
|
||||
<span className={log.success ? "text-emerald-600" : "text-destructive"}>
|
||||
{log.success ? "성공" : "실패"}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -323,7 +323,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
|
|||
<CardTitle className="text-sm font-medium">성공</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
|
|||
<div
|
||||
className={`rounded-md border p-3 text-sm ${
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -527,7 +527,7 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
|
|||
</Button>
|
||||
|
||||
{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>
|
||||
<Label htmlFor="connection_timeout">연결 타임아웃 (초)</Label>
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export function HeadersManager({ headers, onChange }: HeadersManagerProps) {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
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" />
|
||||
</Button>
|
||||
|
|
@ -127,12 +127,12 @@ export function HeadersManager({ headers, onChange }: HeadersManagerProps) {
|
|||
</Table>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
* 공통으로 사용할 HTTP 헤더를 설정합니다. 인증 헤더는 별도의 인증 설정에서 관리됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -239,30 +239,30 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<div className="mb-6 flex items-center justify-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<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
|
||||
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
|
||||
</div>
|
||||
<span className="text-sm font-medium">기본 정보</span>
|
||||
</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 === "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
|
||||
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
|
||||
</div>
|
||||
<span className="text-sm font-medium">템플릿 선택</span>
|
||||
</div>
|
||||
<div className="h-px w-8 bg-gray-300" />
|
||||
<div className={`flex items-center gap-2 ${step === "advanced" ? "text-primary" : "text-gray-400"}`}>
|
||||
<div className="h-px w-8 bg-muted/60" />
|
||||
<div className={`flex items-center gap-2 ${step === "advanced" ? "text-primary" : "text-muted-foreground/70"}`}>
|
||||
<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
|
||||
</div>
|
||||
|
|
@ -305,7 +305,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<Card
|
||||
key={category.id}
|
||||
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 }))}
|
||||
>
|
||||
|
|
@ -314,7 +314,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<IconComponent className="h-5 w-5 text-muted-foreground" />
|
||||
<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>
|
||||
</CardContent>
|
||||
|
|
@ -341,13 +341,13 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<div className="space-y-4">
|
||||
<div>
|
||||
<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">
|
||||
{LAYOUT_TEMPLATES.map((template) => (
|
||||
<Card
|
||||
key={template.id}
|
||||
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={() =>
|
||||
setFormData((prev) => ({
|
||||
|
|
@ -364,8 +364,8 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<Badge variant="secondary">{template.zones}개 영역</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{template.description}</div>
|
||||
<div className="text-xs text-gray-500">예: {template.example}</div>
|
||||
<div className="rounded bg-gray-100 p-2 text-center font-mono text-xs">{template.icon}</div>
|
||||
<div className="text-xs text-muted-foreground">예: {template.example}</div>
|
||||
<div className="rounded bg-muted p-2 text-center font-mono text-xs">{template.icon}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -418,7 +418,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-sm text-gray-500">개 영역</span>
|
||||
<span className="text-sm text-muted-foreground">개 영역</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -428,17 +428,17 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<div className="space-y-4">
|
||||
{generationResult ? (
|
||||
<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" />
|
||||
<AlertDescription className={generationResult.success ? "text-green-800" : "text-red-800"}>
|
||||
<AlertDescription className={generationResult.success ? "text-emerald-800" : "text-red-800"}>
|
||||
{generationResult.message}
|
||||
{generationResult.success && generationResult.files && (
|
||||
<div className="mt-2">
|
||||
<div className="text-sm font-medium">생성된 파일들:</div>
|
||||
<ul className="mt-1 space-y-1 text-xs">
|
||||
{generationResult.files.map((file, index) => (
|
||||
<li key={index} className="text-green-700">
|
||||
<li key={index} className="text-emerald-700">
|
||||
• {file}
|
||||
</li>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -748,7 +748,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
</SelectContent>
|
||||
</Select>
|
||||
{!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>
|
||||
|
||||
|
|
@ -817,7 +817,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
}}
|
||||
>
|
||||
<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>
|
||||
|
|
@ -825,7 +825,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
)}
|
||||
</div>
|
||||
{selectedLangKeyInfo && (
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{getText(MENU_MANAGEMENT_KEYS.FORM_LANG_KEY_SELECTED)
|
||||
.replace("{key}", selectedLangKeyInfo.langKey)
|
||||
.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="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
|
||||
placeholder="화면 검색..."
|
||||
value={screenSearchText}
|
||||
|
|
@ -918,14 +918,14 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
<div
|
||||
key={`screen-${screen.screenId || screen.id || index}-${screen.screenCode || index}`}
|
||||
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>
|
||||
<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 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>
|
||||
))}
|
||||
|
|
@ -933,7 +933,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
(screen) =>
|
||||
screen.screenName.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>
|
||||
)}
|
||||
|
|
@ -942,7 +942,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
{/* 선택된 화면 정보 표시 */}
|
||||
{selectedScreen && (
|
||||
<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">생성된 URL: {formData.menuUrl}</div>
|
||||
</div>
|
||||
|
|
@ -971,7 +971,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
{/* 검색창 */}
|
||||
<div className="border-b p-2">
|
||||
<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
|
||||
type="text"
|
||||
placeholder="대시보드 검색..."
|
||||
|
|
@ -995,13 +995,13 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
<div
|
||||
key={dashboard.id}
|
||||
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>
|
||||
<div className="text-sm font-medium">{dashboard.title}</div>
|
||||
{dashboard.description && (
|
||||
<div className="text-xs text-gray-500">{dashboard.description}</div>
|
||||
<div className="text-xs text-muted-foreground">{dashboard.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1012,7 +1012,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
dashboard.title.toLowerCase().includes(dashboardSearchText.toLowerCase()) ||
|
||||
(dashboard.description &&
|
||||
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>
|
||||
)}
|
||||
|
|
@ -1021,7 +1021,7 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
{/* 선택된 대시보드 정보 표시 */}
|
||||
{selectedDashboard && (
|
||||
<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 && (
|
||||
<div className="text-primary text-xs">설명: {selectedDashboard.description}</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -745,7 +745,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
|||
<span>생성 (C)</span>
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) => handleSelectAll("createYn", checked as boolean)}
|
||||
className="data-[state=checked]:bg-green-600"
|
||||
className="data-[state=checked]:bg-emerald-600"
|
||||
/>
|
||||
</div>
|
||||
</TableHead>
|
||||
|
|
@ -754,7 +754,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
|||
<span>조회 (R)</span>
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) => handleSelectAll("readYn", checked as boolean)}
|
||||
className="data-[state=checked]:bg-blue-600"
|
||||
className="data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
</TableHead>
|
||||
|
|
@ -772,7 +772,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
|||
<span>삭제 (D)</span>
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) => handleSelectAll("deleteYn", checked as boolean)}
|
||||
className="data-[state=checked]:bg-red-600"
|
||||
className="data-[state=checked]:bg-destructive"
|
||||
/>
|
||||
</div>
|
||||
</TableHead>
|
||||
|
|
@ -834,7 +834,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
|||
<Checkbox
|
||||
checked={menu.createYn === "Y"}
|
||||
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 className="flex items-center justify-between text-sm">
|
||||
|
|
@ -842,7 +842,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
|||
<Checkbox
|
||||
checked={menu.readYn === "Y"}
|
||||
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "readYn", checked as boolean)}
|
||||
className="data-[state=checked]:bg-blue-600"
|
||||
className="data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
|
|
@ -858,7 +858,7 @@ export function MenuPermissionsTable({ permissions, onPermissionsChange, roleGro
|
|||
<Checkbox
|
||||
checked={menu.deleteYn === "Y"}
|
||||
onCheckedChange={(checked) => handlePermissionChange(menu.menuObjid, "deleteYn", checked as boolean)}
|
||||
className="data-[state=checked]:bg-red-600"
|
||||
className="data-[state=checked]:bg-destructive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -564,7 +564,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
|||
{testResult && (
|
||||
<div
|
||||
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">
|
||||
|
|
@ -572,17 +572,17 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
|||
{testResult.success ? "성공" : "실패"}
|
||||
</Badge>
|
||||
{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>
|
||||
|
||||
<p className="text-sm">{testResult.message}</p>
|
||||
|
||||
{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>
|
||||
|
|
|
|||
|
|
@ -79,9 +79,9 @@ export function RoleDeleteModal({ isOpen, onClose, onSuccess, role }: RoleDelete
|
|||
|
||||
<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">
|
||||
<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">
|
||||
<p className="text-sm font-semibold text-orange-900">정말로 삭제하시겠습니까?</p>
|
||||
<p className="text-xs text-orange-800">
|
||||
|
|
@ -124,7 +124,7 @@ export function RoleDeleteModal({ isOpen, onClose, onSuccess, role }: RoleDelete
|
|||
<div
|
||||
className={`rounded-lg border p-3 text-sm ${
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
|||
{/* 권한 그룹명 */}
|
||||
<div>
|
||||
<Label htmlFor="authName" className="text-xs sm:text-sm">
|
||||
권한 그룹명 <span className="text-red-500">*</span>
|
||||
권한 그룹명 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="authName"
|
||||
|
|
@ -210,7 +210,7 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
|||
{/* 권한 코드 */}
|
||||
<div>
|
||||
<Label htmlFor="authCode" className="text-xs sm:text-sm">
|
||||
권한 코드 <span className="text-red-500">*</span>
|
||||
권한 코드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="authCode"
|
||||
|
|
@ -243,7 +243,7 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
|||
) : (
|
||||
<div>
|
||||
<Label htmlFor="companyCode" className="text-xs sm:text-sm">
|
||||
회사 <span className="text-red-500">*</span>
|
||||
회사 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
{isSuperAdmin ? (
|
||||
<>
|
||||
|
|
@ -345,10 +345,10 @@ export function RoleFormModal({ isOpen, onClose, onSuccess, editingRole }: RoleF
|
|||
<div
|
||||
className={`rounded-lg border p-3 text-sm ${
|
||||
alertType === "success"
|
||||
? "border-green-300 bg-green-50 text-green-800"
|
||||
? "border-green-300 bg-emerald-50 text-emerald-800"
|
||||
: alertType === "error"
|
||||
? "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">
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
</div>
|
||||
|
||||
{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>
|
||||
<h3 className="font-medium">
|
||||
|
|
@ -267,7 +267,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
<Monitor className="h-5 w-5" />
|
||||
할당된 화면 ({assignedScreens.length}개)
|
||||
</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" />
|
||||
화면 할당
|
||||
</Button>
|
||||
|
|
@ -275,15 +275,15 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="py-8 text-center text-gray-500">로딩 중...</div>
|
||||
<div className="py-8 text-center text-muted-foreground">로딩 중...</div>
|
||||
) : 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">
|
||||
{assignedScreens.map((screen) => (
|
||||
<div
|
||||
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 items-center gap-3">
|
||||
|
|
@ -298,7 +298,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
테이블: {screen.tableName} | 생성일: {screen.createdDate.toLocaleDateString()}
|
||||
</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>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -307,7 +307,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
setSelectedScreen(screen);
|
||||
setShowUnassignDialog(true);
|
||||
}}
|
||||
className="text-destructive hover:text-red-700"
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
@ -330,7 +330,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
<div className="space-y-4">
|
||||
{/* 검색 */}
|
||||
<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
|
||||
placeholder="화면명 또는 코드로 검색..."
|
||||
value={searchTerm}
|
||||
|
|
@ -342,13 +342,13 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
{/* 화면 목록 */}
|
||||
<div className="max-h-64 space-y-2 overflow-y-auto">
|
||||
{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) => (
|
||||
<div
|
||||
key={screen.screenId}
|
||||
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)}
|
||||
>
|
||||
|
|
@ -385,7 +385,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<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>
|
||||
</AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export function SortableCodeItem({
|
|||
}}
|
||||
onPointerDown={(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 ? "접기" : "펼치기"}
|
||||
>
|
||||
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
|
|
@ -135,12 +135,12 @@ export function SortableCodeItem({
|
|||
</Badge>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
|
|
@ -196,7 +196,7 @@ export function SortableCodeItem({
|
|||
onAddChild();
|
||||
}}
|
||||
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" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -102,11 +102,11 @@ export function TableLogViewer({ tableName, open, onOpenChange }: TableLogViewer
|
|||
const getOperationBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case "INSERT":
|
||||
return <Badge className="bg-green-500">추가</Badge>;
|
||||
return <Badge className="bg-emerald-500">추가</Badge>;
|
||||
case "UPDATE":
|
||||
return <Badge className="bg-blue-500">수정</Badge>;
|
||||
return <Badge className="bg-primary">수정</Badge>;
|
||||
case "DELETE":
|
||||
return <Badge className="bg-red-500">삭제</Badge>;
|
||||
return <Badge className="bg-destructive">삭제</Badge>;
|
||||
default:
|
||||
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>
|
||||
<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__" 사용 */}
|
||||
<Select
|
||||
value={operationType || "__all__"}
|
||||
|
|
@ -169,22 +169,22 @@ export function TableLogViewer({ tableName, open, onOpenChange }: TableLogViewer
|
|||
</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)} />
|
||||
</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)} />
|
||||
</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)} />
|
||||
</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)} />
|
||||
</div>
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ export function TableLogViewer({ tableName, open, onOpenChange }: TableLogViewer
|
|||
<LoadingSpinner />
|
||||
</div>
|
||||
) : 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>
|
||||
<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="text-sm text-gray-600">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
전체 {total}건 (페이지 {page} / {totalPages})
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
|
|||
|
|
@ -157,12 +157,12 @@ export function TemplateImportExport({ onTemplateImported }: TemplateImportExpor
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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}
|
||||
>
|
||||
<Upload className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
||||
<p className="mb-2 text-lg font-medium text-gray-900">템플릿 JSON 파일을 선택하세요</p>
|
||||
<p className="text-sm text-gray-500">또는 아래에 JSON 내용을 직접 입력하세요</p>
|
||||
<Upload className="mx-auto mb-4 h-12 w-12 text-muted-foreground/70" />
|
||||
<p className="mb-2 text-lg font-medium text-foreground">템플릿 JSON 파일을 선택하세요</p>
|
||||
<p className="text-sm text-muted-foreground">또는 아래에 JSON 내용을 직접 입력하세요</p>
|
||||
</div>
|
||||
<input ref={fileInputRef} type="file" accept=".json" onChange={handleFileUpload} className="hidden" />
|
||||
</CardContent>
|
||||
|
|
@ -196,7 +196,7 @@ export function TemplateImportExport({ onTemplateImported }: TemplateImportExpor
|
|||
<Card>
|
||||
<CardHeader>
|
||||
<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. 템플릿 미리보기
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
|
|||
|
|
@ -54,28 +54,28 @@ export function UserAuthEditModal({ isOpen, onClose, onSuccess, user }: UserAuth
|
|||
label: "회사 관리자",
|
||||
description: "자기 회사 데이터 및 사용자 관리 가능",
|
||||
icon: <Building2 className="h-4 w-4" />,
|
||||
color: "text-blue-600",
|
||||
color: "text-primary",
|
||||
},
|
||||
{
|
||||
value: "USER",
|
||||
label: "일반 사용자",
|
||||
description: "자기 회사 데이터 조회/수정만 가능",
|
||||
icon: <User className="h-4 w-4" />,
|
||||
color: "text-gray-600",
|
||||
color: "text-muted-foreground",
|
||||
},
|
||||
{
|
||||
value: "GUEST",
|
||||
label: "게스트",
|
||||
description: "제한된 조회 권한",
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
color: "text-green-600",
|
||||
color: "text-emerald-600",
|
||||
},
|
||||
{
|
||||
value: "PARTNER",
|
||||
label: "협력업체",
|
||||
description: "협력업체 전용 권한",
|
||||
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">
|
||||
<Label htmlFor="userType" className="text-sm font-medium">
|
||||
새로운 권한 <span className="text-red-500">*</span>
|
||||
새로운 권한 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select value={selectedUserType} onValueChange={setSelectedUserType}>
|
||||
<SelectTrigger className="h-10">
|
||||
|
|
@ -180,9 +180,9 @@ export function UserAuthEditModal({ isOpen, onClose, onSuccess, user }: UserAuth
|
|||
|
||||
{/* 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">
|
||||
<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">
|
||||
<p className="text-sm font-semibold text-orange-900">최고 관리자 권한 부여 경고</p>
|
||||
<p className="text-xs text-orange-800">
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertMod
|
|||
const getTypeColor = () => {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return "text-green-600";
|
||||
return "text-emerald-600";
|
||||
case "error":
|
||||
return "text-destructive";
|
||||
default:
|
||||
|
|
@ -446,7 +446,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserF
|
|||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="userId" className="text-sm font-medium">
|
||||
사용자 ID <span className="text-red-500">*</span>
|
||||
사용자 ID <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
{isEditMode ? (
|
||||
<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 && (
|
||||
<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}
|
||||
</div>
|
||||
|
|
@ -485,7 +485,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserF
|
|||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="userName" className="text-sm font-medium">
|
||||
사용자명 <span className="text-red-500">*</span>
|
||||
사용자명 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="userName"
|
||||
|
|
@ -501,7 +501,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserF
|
|||
{!isEditMode && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="userPassword" className="text-sm font-medium">
|
||||
비밀번호 <span className="text-red-500">*</span>
|
||||
비밀번호 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
|
|
@ -532,7 +532,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess, editingUser }: UserF
|
|||
{/* 회사 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="companyCode" className="text-sm font-medium">
|
||||
회사 <span className="text-red-500">*</span>
|
||||
회사 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
{isSuperAdmin ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
|
|||
<div className="space-y-4" onKeyDown={handleKeyDown}>
|
||||
{/* 대상 사용자 정보 */}
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-gray-700">
|
||||
<Label className="text-sm font-medium text-foreground">
|
||||
대상 사용자:{" "}
|
||||
<span className="font-semibold">
|
||||
{userName} ({userId})
|
||||
|
|
@ -197,10 +197,10 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
|
|||
|
||||
{/* 비밀번호 일치 여부 표시 */}
|
||||
{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 className="text-xs text-gray-500">* 비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.</div>
|
||||
<div className="text-xs text-muted-foreground">* 비밀번호는 영문, 숫자, 특수문자만 사용할 수 있습니다.</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-2">
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function DriverListView({ drivers, config, isCompact = false }: DriverLis
|
|||
|
||||
return (
|
||||
<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">
|
||||
<tr>
|
||||
{visibleColumns.includes("status") && (
|
||||
|
|
@ -96,7 +96,7 @@ export function DriverListView({ drivers, config, isCompact = false }: DriverLis
|
|||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-background">
|
||||
<tbody className="divide-y divide-border bg-background">
|
||||
{drivers.map((driver) => {
|
||||
const statusColors = getStatusColor(driver.status);
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -180,16 +180,16 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
const renderIcon = (icon?: string, color?: string) => {
|
||||
const colorClass =
|
||||
color === "blue"
|
||||
? "text-blue-600"
|
||||
? "text-primary"
|
||||
: color === "orange"
|
||||
? "text-orange-600"
|
||||
? "text-amber-600"
|
||||
: color === "green"
|
||||
? "text-green-600"
|
||||
? "text-emerald-600"
|
||||
: color === "red"
|
||||
? "text-red-600"
|
||||
? "text-destructive"
|
||||
: color === "purple"
|
||||
? "text-purple-600"
|
||||
: "text-gray-600";
|
||||
: "text-muted-foreground";
|
||||
|
||||
switch (icon) {
|
||||
case "truck":
|
||||
|
|
@ -209,16 +209,16 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
const renderFieldGroup = (group: FieldGroup, data: Record<string, any>) => {
|
||||
const colorClass =
|
||||
group.color === "blue"
|
||||
? "text-blue-600"
|
||||
? "text-primary"
|
||||
: group.color === "orange"
|
||||
? "text-orange-600"
|
||||
? "text-amber-600"
|
||||
: group.color === "green"
|
||||
? "text-green-600"
|
||||
? "text-emerald-600"
|
||||
: group.color === "red"
|
||||
? "text-red-600"
|
||||
? "text-destructive"
|
||||
: group.color === "purple"
|
||||
? "text-purple-600"
|
||||
: "text-gray-600";
|
||||
: "text-muted-foreground";
|
||||
|
||||
return (
|
||||
<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">
|
||||
<span className="text-muted-foreground text-sm font-medium">도구:</span>
|
||||
{[
|
||||
{ type: "area" as ToolType, label: "영역", icon: Grid3x3, color: "text-blue-500" },
|
||||
{ type: "location-bed" as ToolType, label: "베드", icon: Package, color: "text-blue-600" },
|
||||
{ type: "location-stp" as ToolType, label: "정차", icon: Move, color: "text-gray-500" },
|
||||
// { type: "crane-gantry" as ToolType, label: "겐트리", icon: Combine, color: "text-green-500" },
|
||||
{ type: "crane-mobile" as ToolType, label: "크레인", icon: Truck, color: "text-yellow-500" },
|
||||
{ type: "area" as ToolType, label: "영역", icon: Grid3x3, color: "text-primary" },
|
||||
{ type: "location-bed" as ToolType, label: "베드", icon: Package, color: "text-primary" },
|
||||
{ type: "location-stp" as ToolType, label: "정차", icon: Move, color: "text-muted-foreground" },
|
||||
// { type: "crane-gantry" as ToolType, label: "겐트리", icon: Combine, color: "text-emerald-500" },
|
||||
{ type: "crane-mobile" as ToolType, label: "크레인", icon: Truck, color: "text-amber-500" },
|
||||
{ type: "rack" as ToolType, label: "랙", icon: Box, color: "text-purple-500" },
|
||||
].map((tool) => {
|
||||
const Icon = tool.icon;
|
||||
|
|
@ -1919,7 +1919,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
</div>
|
||||
</div>
|
||||
{isLocationPlaced ? (
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
<Check className="h-4 w-4 text-emerald-500" />
|
||||
) : locationType === "location-stp" ? (
|
||||
<ParkingCircle className="text-muted-foreground h-4 w-4" />
|
||||
) : (
|
||||
|
|
@ -2052,7 +2052,7 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
|
|||
</div>
|
||||
|
||||
{/* 중앙: 3D 캔버스 */}
|
||||
<div className="relative h-full flex-1 bg-gray-100">
|
||||
<div className="relative h-full flex-1 bg-muted">
|
||||
{isLoading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
||||
|
|
|
|||
|
|
@ -716,7 +716,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
</p>
|
||||
)}
|
||||
{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>
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -802,7 +802,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
|
|||
</p>
|
||||
)}
|
||||
{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>
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1239,7 +1239,7 @@ export default function Yard3DCanvas({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-gray-100" onClick={handleCanvasClick}>
|
||||
<div className="h-full w-full bg-muted" onClick={handleCanvasClick}>
|
||||
<Canvas
|
||||
camera={{
|
||||
position: [50, 30, 50],
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ export const OBJECT_COLORS: Record<string, string> = {
|
|||
|
||||
// Tailwind 색상 클래스 매핑 (아이콘용)
|
||||
export const OBJECT_COLOR_CLASSES: Record<string, string> = {
|
||||
area: "text-blue-500",
|
||||
"location-bed": "text-blue-600",
|
||||
"location-stp": "text-gray-500",
|
||||
"location-temp": "text-orange-500",
|
||||
area: "text-primary",
|
||||
"location-bed": "text-primary",
|
||||
"location-stp": "text-muted-foreground",
|
||||
"location-temp": "text-amber-500",
|
||||
"location-dest": "text-emerald-500",
|
||||
"crane-mobile": "text-purple-500",
|
||||
rack: "text-red-500",
|
||||
rack: "text-destructive",
|
||||
};
|
||||
|
||||
// 기본 색상
|
||||
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" />
|
||||
)
|
||||
) : (
|
||||
<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
|
||||
? "border-destructive bg-destructive/10"
|
||||
: preview?.isOverride
|
||||
? "border-blue-500 bg-blue-500/10"
|
||||
: "border-green-500 bg-green-500/10"
|
||||
? "border-primary bg-primary/10"
|
||||
: "border-emerald-500 bg-emerald-500/10"
|
||||
)}>
|
||||
<div className="flex items-center gap-2">
|
||||
{previewLoading ? (
|
||||
|
|
@ -318,9 +318,9 @@ export function KeyGenerateModal({
|
|||
) : preview?.exists ? (
|
||||
<AlertCircle className="h-4 w-4 text-destructive" />
|
||||
) : 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">
|
||||
{generatedKeyPreview}
|
||||
|
|
@ -332,7 +332,7 @@ export function KeyGenerateModal({
|
|||
</p>
|
||||
)}
|
||||
{preview?.isOverride && !preview?.exists && (
|
||||
<p className="mt-1 text-xs text-blue-600">
|
||||
<p className="mt-1 text-xs text-primary">
|
||||
공통 키가 존재합니다. 회사별 오버라이드로 생성됩니다.
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ export function ErrorMessage({ message }: ErrorMessageProps) {
|
|||
if (!message) return null;
|
||||
|
||||
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 (
|
||||
<div
|
||||
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" : ""
|
||||
}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function BarcodeDesignerCanvas() {
|
|||
}), [widthPx, heightPx, components.length, addComponent, snapValueToGrid]);
|
||||
|
||||
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
|
||||
key={`canvas-${widthMm}-${heightMm}`}
|
||||
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>
|
||||
);
|
||||
|
|
@ -244,7 +244,7 @@ export function BarcodeLabelCanvasComponent({ component }: Props) {
|
|||
{selected && component.type !== "line" && (
|
||||
<div
|
||||
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) => {
|
||||
e.stopPropagation();
|
||||
setIsResizing(true);
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ export function BarcodePrintPreviewModal({
|
|||
</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
|
||||
className="relative bg-white shadow"
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -27,26 +27,26 @@ interface AlertModalProps {
|
|||
const alertConfig = {
|
||||
success: {
|
||||
icon: CheckCircle,
|
||||
iconColor: "text-green-500",
|
||||
titleColor: "text-green-700",
|
||||
iconColor: "text-emerald-500",
|
||||
titleColor: "text-emerald-700",
|
||||
buttonVariant: "default" as const,
|
||||
},
|
||||
error: {
|
||||
icon: XCircle,
|
||||
iconColor: "text-red-500",
|
||||
titleColor: "text-red-700",
|
||||
iconColor: "text-destructive",
|
||||
titleColor: "text-destructive",
|
||||
buttonVariant: "destructive" as const,
|
||||
},
|
||||
warning: {
|
||||
icon: AlertTriangle,
|
||||
iconColor: "text-yellow-500",
|
||||
iconColor: "text-amber-500",
|
||||
titleColor: "text-yellow-700",
|
||||
buttonVariant: "default" as const,
|
||||
},
|
||||
info: {
|
||||
icon: Info,
|
||||
iconColor: "text-blue-500",
|
||||
titleColor: "text-blue-700",
|
||||
iconColor: "text-primary",
|
||||
titleColor: "text-primary",
|
||||
buttonVariant: "default" as const,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1131,7 +1131,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
<FileSpreadsheet className="h-5 w-5" />
|
||||
엑셀 데이터 업로드
|
||||
{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>
|
||||
)}
|
||||
|
|
@ -1322,15 +1322,15 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
isDragOver
|
||||
? "border-primary bg-primary/5"
|
||||
: file
|
||||
? "border-green-500 bg-green-50"
|
||||
? "border-emerald-500 bg-emerald-50"
|
||||
: "border-muted-foreground/25 hover:border-primary hover:bg-muted/50"
|
||||
)}
|
||||
>
|
||||
{file ? (
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -1561,11 +1561,11 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
|
||||
{/* 중복 체크 안내 */}
|
||||
{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 gap-2">
|
||||
<Copy className="mt-0.5 h-4 w-4 text-blue-600" />
|
||||
<div className="text-[10px] text-blue-700 sm:text-xs">
|
||||
<Copy className="mt-0.5 h-4 w-4 text-primary" />
|
||||
<div className="text-[10px] text-primary sm:text-xs">
|
||||
<p className="font-medium">
|
||||
중복 키: {columnMappings
|
||||
.filter((m) => m.checkDuplicate && m.systemColumn)
|
||||
|
|
@ -1581,12 +1581,12 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
<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
|
||||
value={duplicateAction}
|
||||
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 />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -1686,7 +1686,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
<span className="font-medium">{mapping.excelColumn}</span> →{" "}
|
||||
{col?.label || mapping.systemColumn}
|
||||
{mapping.checkDuplicate && (
|
||||
<span className="ml-2 text-blue-600">
|
||||
<span className="ml-2 text-primary">
|
||||
(중복 체크: {mapping.duplicateAction === "overwrite" ? "덮어쓰기" : "건너뛰기"})
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1701,9 +1701,9 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
|
||||
{/* 중복 체크 요약 */}
|
||||
{duplicateCheckCount > 0 && (
|
||||
<div className="rounded-md border border-blue-200 bg-blue-50 p-4">
|
||||
<h3 className="text-sm font-medium text-blue-800 sm:text-base">중복 체크 설정</h3>
|
||||
<div className="mt-2 space-y-1 text-[10px] text-blue-700 sm:text-xs">
|
||||
<div className="rounded-md border border-primary/20 bg-primary/10 p-4">
|
||||
<h3 className="text-sm font-medium text-primary sm:text-base">중복 체크 설정</h3>
|
||||
<div className="mt-2 space-y-1 text-[10px] text-primary sm:text-xs">
|
||||
<p>
|
||||
<span className="font-medium">중복 키:</span>{" "}
|
||||
{columnMappings
|
||||
|
|
|
|||
|
|
@ -140,9 +140,9 @@ const CompactValidationIndicator: React.FC<{
|
|||
<span className="text-destructive">{validationState.errors.length}개 오류</span>
|
||||
)}
|
||||
{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>
|
||||
|
||||
<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,
|
||||
icon: CheckCircle,
|
||||
text: "유효함",
|
||||
className: "bg-green-500 hover:bg-green-600",
|
||||
className: "bg-emerald-500 hover:bg-emerald-600",
|
||||
};
|
||||
case "invalid":
|
||||
return {
|
||||
|
|
@ -238,14 +238,14 @@ const ValidationSummary: React.FC<{ validationState: FormValidationState }> = ({
|
|||
)}
|
||||
|
||||
{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" />
|
||||
<span>{validationState.warnings.length}개 경고</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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" />
|
||||
<span>모든 검증 통과</span>
|
||||
</div>
|
||||
|
|
@ -307,8 +307,8 @@ const ValidationErrorItem: React.FC<{ error: ValidationError }> = ({ error }) =>
|
|||
*/
|
||||
const ValidationWarningItem: React.FC<{ warning: ValidationWarning }> = ({ warning }) => {
|
||||
return (
|
||||
<Alert className="border-orange-200 bg-orange-50 py-2">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-600" />
|
||||
<Alert className="border-orange-200 bg-amber-50 py-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||
<AlertDescription className="text-sm">
|
||||
<span className="font-medium">{warning.field}:</span> {warning.message}
|
||||
{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 && (
|
||||
<>
|
||||
{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" />}
|
||||
{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>}
|
||||
|
||||
{warning && !error && <span className="text-orange-600">{warning.message}</span>}
|
||||
{warning && !error && <span className="text-amber-600">{warning.message}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1028,7 +1028,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
{loading ? (
|
||||
<div className="flex h-full items-center justify-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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -102,26 +102,26 @@ export function TableHistoryModal({
|
|||
const getOperationIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "INSERT":
|
||||
return <Plus className="h-4 w-4 text-green-600" />;
|
||||
return <Plus className="h-4 w-4 text-emerald-600" />;
|
||||
case "UPDATE":
|
||||
return <FileEdit className="h-4 w-4 text-blue-600" />;
|
||||
return <FileEdit className="h-4 w-4 text-primary" />;
|
||||
case "DELETE":
|
||||
return <Trash2 className="h-4 w-4 text-red-600" />;
|
||||
return <Trash2 className="h-4 w-4 text-destructive" />;
|
||||
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) => {
|
||||
switch (type) {
|
||||
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":
|
||||
return <span className="text-sm font-medium text-blue-600">수정</span>;
|
||||
return <span className="text-sm font-medium text-primary">수정</span>;
|
||||
case "DELETE":
|
||||
return <span className="text-sm font-medium text-red-600">삭제</span>;
|
||||
return <span className="text-sm font-medium text-destructive">삭제</span>;
|
||||
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">
|
||||
{timeline.map((event, index) => (
|
||||
<div key={index} className="relative border-l-2 border-gray-200 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 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-border bg-white p-1">
|
||||
{getOperationIcon(event.operation_type)}
|
||||
</div>
|
||||
|
||||
|
|
@ -325,12 +325,12 @@ export function TableHistoryModal({
|
|||
<p className="text-muted-foreground text-xs font-medium">변경된 항목:</p>
|
||||
<div className="space-y-1">
|
||||
{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>
|
||||
<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 className="font-medium text-green-600">{change.newValue || "(없음)"}</span>
|
||||
<span className="font-medium text-emerald-600">{change.newValue || "(없음)"}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -355,7 +355,7 @@ export function TableHistoryModal({
|
|||
</div>
|
||||
) : (
|
||||
<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>
|
||||
{!recordId && <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) => {
|
||||
const displayValue = getDisplayValue(record);
|
||||
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 && (
|
||||
<td className="p-2">
|
||||
{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>
|
||||
)}
|
||||
|
|
@ -382,8 +382,8 @@ export function TableHistoryModal({
|
|||
)}
|
||||
<td className="p-2">{getOperationBadge(record.operation_type)}</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-green-600">{record.new_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-emerald-600">{record.new_value || "-"}</td>
|
||||
<td className="p-2">{record.changed_by}</td>
|
||||
<td className="text-muted-foreground p-2">{formatDate(record.changed_at)}</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface ValidationMessageProps {
|
|||
|
||||
export function ValidationMessage({ message, isValid, isLoading, className }: ValidationMessageProps) {
|
||||
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) {
|
||||
|
|
@ -18,6 +18,6 @@ export function ValidationMessage({ message, isValid, isLoading, className }: Va
|
|||
}
|
||||
|
||||
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 colorClass =
|
||||
color === "blue"
|
||||
? "text-blue-600"
|
||||
? "text-primary"
|
||||
: color === "orange"
|
||||
? "text-orange-600"
|
||||
? "text-amber-600"
|
||||
: color === "green"
|
||||
? "text-green-600"
|
||||
? "text-emerald-600"
|
||||
: color === "red"
|
||||
? "text-red-600"
|
||||
? "text-destructive"
|
||||
: color === "purple"
|
||||
? "text-purple-600"
|
||||
: "text-gray-600";
|
||||
: "text-muted-foreground";
|
||||
|
||||
switch (icon) {
|
||||
case "truck":
|
||||
|
|
@ -241,16 +241,16 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
|||
const renderFieldGroup = (group: FieldGroup, groupData: Record<string, any>) => {
|
||||
const colorClass =
|
||||
group.color === "blue"
|
||||
? "text-blue-600"
|
||||
? "text-primary"
|
||||
: group.color === "orange"
|
||||
? "text-orange-600"
|
||||
? "text-amber-600"
|
||||
: group.color === "green"
|
||||
? "text-green-600"
|
||||
? "text-emerald-600"
|
||||
: group.color === "red"
|
||||
? "text-red-600"
|
||||
? "text-destructive"
|
||||
: group.color === "purple"
|
||||
? "text-purple-600"
|
||||
: "text-gray-600";
|
||||
: "text-muted-foreground";
|
||||
|
||||
return (
|
||||
<div key={group.id} className="rounded-lg border p-4">
|
||||
|
|
|
|||
|
|
@ -1352,8 +1352,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
|
||||
{/* 이동경로 날짜 선택 */}
|
||||
{selectedUserId && (
|
||||
<div className="flex items-center gap-1 rounded border bg-blue-50 px-2 py-1">
|
||||
<span className="text-xs text-blue-600">🛣️</span>
|
||||
<div className="flex items-center gap-1 rounded border bg-primary/10 px-2 py-1">
|
||||
<span className="text-xs text-primary">🛣️</span>
|
||||
<input
|
||||
type="date"
|
||||
value={routeDate}
|
||||
|
|
@ -1363,10 +1363,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
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>
|
||||
<button onClick={clearRoute} className="ml-1 text-xs text-blue-400 hover:text-blue-600">
|
||||
<span className="text-xs text-primary">({routePoints.length}개)</span>
|
||||
<button onClick={clearRoute} className="ml-1 text-xs text-primary/80 hover:text-primary">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1494,15 +1494,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
const parsed = JSON.parse(matchingPolygon.description);
|
||||
popupContent = `
|
||||
<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>` : ""}
|
||||
<div class="bg-gray-100 rounded p-2">
|
||||
<div class="text-gray-900 mb-1 text-xs font-semibold">상세 정보</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-muted rounded p-2">
|
||||
<div class="text-foreground mb-1 text-xs font-semibold">상세 정보</div>
|
||||
<div class="space-y-2">
|
||||
${popupFields
|
||||
.map((field) => {
|
||||
const value = parsed[field.fieldName];
|
||||
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("")}
|
||||
</div>
|
||||
|
|
@ -1980,7 +1980,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
<button
|
||||
onClick={() => loadTripInfo(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 ? "로딩 중..." : "📊 운행/공차 정보 보기"}
|
||||
</button>
|
||||
|
|
@ -1996,10 +1996,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
{/* 운행 정보 */}
|
||||
{hasTripInfo && (
|
||||
<div className="mb-2">
|
||||
<div className="text-xs font-semibold text-blue-600 mb-1">🚛 최근 운행</div>
|
||||
<div className="bg-blue-50 rounded p-2 space-y-1">
|
||||
<div className="text-xs font-semibold text-primary mb-1">🚛 최근 운행</div>
|
||||
<div className="bg-primary/10 rounded p-2 space-y-1">
|
||||
{(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>{" "}
|
||||
{formatDateTime(info.last_trip_start)} ~ {formatDateTime(info.last_trip_end)}
|
||||
</div>
|
||||
|
|
@ -2007,20 +2007,20 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
<div className="flex gap-3 text-[10px]">
|
||||
{info.last_trip_distance !== undefined && info.last_trip_distance !== null && (
|
||||
<span>
|
||||
<span className="font-medium text-gray-600">거리:</span>{" "}
|
||||
<span className="text-blue-700 font-semibold">{formatDistance(info.last_trip_distance)}</span>
|
||||
<span className="font-medium text-muted-foreground">거리:</span>{" "}
|
||||
<span className="text-primary font-semibold">{formatDistance(info.last_trip_distance)}</span>
|
||||
</span>
|
||||
)}
|
||||
{info.last_trip_time !== undefined && info.last_trip_time !== null && (
|
||||
<span>
|
||||
<span className="font-medium text-gray-600">소요:</span>{" "}
|
||||
<span className="text-blue-700 font-semibold">{formatTime(info.last_trip_time)}</span>
|
||||
<span className="font-medium text-muted-foreground">소요:</span>{" "}
|
||||
<span className="text-primary font-semibold">{formatTime(info.last_trip_time)}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 출발지/도착지 */}
|
||||
{(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 && info.arrival && " → "}
|
||||
{info.arrival && <span>도착: {info.arrival}</span>}
|
||||
|
|
@ -2033,10 +2033,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
{/* 공차 정보 */}
|
||||
{hasEmptyTripInfo && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-orange-600 mb-1">📦 최근 공차</div>
|
||||
<div className="bg-orange-50 rounded p-2 space-y-1">
|
||||
<div className="text-xs font-semibold text-amber-600 mb-1">📦 최근 공차</div>
|
||||
<div className="bg-amber-50 rounded p-2 space-y-1">
|
||||
{(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>{" "}
|
||||
{formatDateTime(info.last_empty_start)} ~ {formatDateTime(info.last_empty_end)}
|
||||
</div>
|
||||
|
|
@ -2044,13 +2044,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
<div className="flex gap-3 text-[10px]">
|
||||
{info.last_empty_distance !== undefined && info.last_empty_distance !== null && (
|
||||
<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>
|
||||
)}
|
||||
{info.last_empty_time !== undefined && info.last_empty_time !== null && (
|
||||
<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>
|
||||
)}
|
||||
|
|
@ -2085,7 +2085,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
|||
<button
|
||||
onClick={() => loadRoute(visibleUserId)}
|
||||
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 ? "로딩 중..." : "🛣️ 이동경로 보기"}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
|||
<button
|
||||
onClick={() => loadRoute(vehicle)}
|
||||
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
|
||||
? "로딩 중..."
|
||||
|
|
@ -391,7 +391,7 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
|||
|
||||
{/* 이동경로 정보 표시 - 상단으로 이동하여 주석 처리 */}
|
||||
{/* {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="text-xs text-white">
|
||||
<div className="font-semibold">🛣️ {selectedVehicle.name} 이동경로</div>
|
||||
|
|
|
|||
|
|
@ -733,13 +733,13 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<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>
|
||||
<AlertDialogDescription className="text-base">
|
||||
<span className="font-medium text-green-600">{createdConnectionName}</span> 연결이 생성되었습니다.
|
||||
<span className="font-medium text-emerald-600">{createdConnectionName}</span> 연결이 생성되었습니다.
|
||||
<br />
|
||||
<span className="mt-2 block text-sm text-gray-500">
|
||||
<span className="mt-2 block text-sm text-muted-foreground">
|
||||
생성된 연결은 데이터플로우 다이어그램에서 확인할 수 있습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
|
|
|
|||
|
|
@ -800,7 +800,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
]);
|
||||
|
||||
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">
|
||||
{/* 사이드바 */}
|
||||
<DataFlowSidebar
|
||||
|
|
@ -892,12 +892,12 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
{/* 안내 메시지 */}
|
||||
{nodes.length === 0 && (
|
||||
<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-1 text-lg font-medium">테이블 간 데이터 관계 설정을 시작하세요</div>
|
||||
<div className="text-sm">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
|||
getSelectedTableNames,
|
||||
}) => {
|
||||
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">
|
||||
<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()} />
|
||||
|
|
@ -46,7 +46,7 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
|||
<div className="space-y-3">
|
||||
<button
|
||||
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}
|
||||
>
|
||||
🧹 고립된 노드 정리
|
||||
|
|
@ -54,14 +54,14 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
|||
|
||||
<button
|
||||
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
|
||||
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" : ""
|
||||
}`}
|
||||
>
|
||||
|
|
@ -70,8 +70,8 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
|||
</div>
|
||||
|
||||
{/* 통계 정보 */}
|
||||
<div className="mt-6 rounded-lg bg-gray-50 p-4">
|
||||
<div className="mb-2 text-sm font-semibold text-gray-700">통계</div>
|
||||
<div className="mt-6 rounded-lg bg-muted p-4">
|
||||
<div className="mb-2 text-sm font-semibold text-foreground">통계</div>
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<div className="flex justify-between">
|
||||
<span>테이블 노드:</span>
|
||||
|
|
@ -83,7 +83,7 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
|||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>메모리 관계:</span>
|
||||
<span className="font-medium text-orange-600">{tempRelationships.length}개</span>
|
||||
<span className="font-medium text-amber-600">{tempRelationships.length}개</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>관계도 ID:</span>
|
||||
|
|
@ -98,7 +98,7 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
|||
</span>
|
||||
</div>
|
||||
{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>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
|||
|
||||
return (
|
||||
<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={{
|
||||
left: position.x - 160,
|
||||
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 h-8 w-8 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
|
||||
<span className="text-sm text-white">🔗</span>
|
||||
|
|
@ -52,10 +52,10 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
|||
</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="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">
|
||||
{edgeInfo.connectionType}
|
||||
</div>
|
||||
|
|
@ -68,7 +68,7 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
|||
{/* From 테이블 */}
|
||||
<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-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="flex flex-wrap gap-2">
|
||||
{edgeInfo.fromColumns.map((column, index) => (
|
||||
|
|
@ -89,15 +89,15 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
|||
</div>
|
||||
|
||||
{/* To 테이블 */}
|
||||
<div className="rounded-lg border-l-4 border-blue-400 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-base font-bold text-gray-800">{edgeInfo.toTable}</div>
|
||||
<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-primary uppercase">TO</div>
|
||||
<div className="mb-2 text-base font-bold text-foreground">{edgeInfo.toTable}</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{edgeInfo.toColumns.map((column, index) => (
|
||||
<span
|
||||
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}
|
||||
</span>
|
||||
|
|
@ -108,16 +108,16 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
|||
</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
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
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>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -136,16 +136,16 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
|||
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="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="rounded-full bg-primary/20 p-1">
|
||||
<span className="text-sm text-primary">🔗</span>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-gray-800">테이블 간 관계 목록</div>
|
||||
<div className="text-sm font-semibold text-foreground">테이블 간 관계 목록</div>
|
||||
</div>
|
||||
<button
|
||||
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">
|
||||
<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) => (
|
||||
<div
|
||||
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">
|
||||
<h4 className="text-sm font-medium text-gray-900">
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
{relationship.relationshipName || `${relationship.fromTable} → ${relationship.toTable}`}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -172,7 +172,7 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
|||
e.stopPropagation();
|
||||
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="관계 편집"
|
||||
>
|
||||
<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();
|
||||
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="관계 삭제"
|
||||
>
|
||||
<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 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-2xl font-bold text-primary">{relationships.length}</div>
|
||||
<div className="text-sm text-muted-foreground">관계 수</div>
|
||||
</div>
|
||||
<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>
|
||||
<div className="text-center">
|
||||
|
|
@ -207,7 +207,7 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
{relationships.map((relationship, index) => (
|
||||
<div
|
||||
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 items-center gap-2 text-sm">
|
||||
|
|
@ -234,10 +234,10 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
|
||||
{/* 관계가 없는 경우 안내 */}
|
||||
{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="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>
|
||||
|
|
@ -249,7 +249,7 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || relationships.length === 0}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
className="bg-primary hover:bg-primary/90"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -269,13 +269,13 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<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>
|
||||
<AlertDialogDescription className="text-base">
|
||||
<span className="font-medium text-green-600">{savedDiagramName}</span> 관계도가 성공적으로 저장되었습니다.
|
||||
<span className="font-medium text-emerald-600">{savedDiagramName}</span> 관계도가 성공적으로 저장되었습니다.
|
||||
<br />
|
||||
<span className="mt-2 block text-sm text-gray-500">
|
||||
<span className="mt-2 block text-sm text-muted-foreground">
|
||||
저장된 관계도는 관리 메뉴에서 확인하고 수정할 수 있습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
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="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 h-6 w-6 items-center justify-center rounded-full bg-primary/20">
|
||||
<span className="text-sm text-primary">📋</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-800">선택된 테이블</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="text-sm font-semibold text-foreground">선택된 테이블</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{selectedNodes.length === 1
|
||||
? "FROM 테이블 선택됨"
|
||||
: selectedNodes.length === 2
|
||||
|
|
@ -44,7 +44,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
</div>
|
||||
<button
|
||||
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>
|
||||
|
|
@ -66,14 +66,14 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
index === 0
|
||||
? "border-l-4 border-emerald-400 bg-emerald-50"
|
||||
: index === 1
|
||||
? "border-l-4 border-blue-400 bg-accent"
|
||||
? "border-l-4 border-primary/60 bg-accent"
|
||||
: "bg-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<div
|
||||
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}
|
||||
|
|
@ -81,7 +81,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
{selectedNodes.length === 2 && (
|
||||
<div
|
||||
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"}
|
||||
|
|
@ -94,7 +94,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
{/* 연결 화살표 (마지막이 아닌 경우) */}
|
||||
{index < selectedNodes.length - 1 && (
|
||||
<div className="flex justify-center py-1">
|
||||
<div className="text-gray-400">→</div>
|
||||
<div className="text-muted-foreground/70">→</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -104,14 +104,14 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
</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
|
||||
onClick={onOpenConnectionModal}
|
||||
disabled={!canCreateConnection}
|
||||
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
|
||||
canCreateConnection
|
||||
? "bg-accent0 text-white hover:bg-blue-600"
|
||||
: "cursor-not-allowed bg-gray-300 text-gray-500"
|
||||
? "bg-accent0 text-white hover:bg-primary"
|
||||
: "cursor-not-allowed bg-muted/60 text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<span>🔗</span>
|
||||
|
|
@ -119,7 +119,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
</button>
|
||||
<button
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -32,13 +32,13 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
const { table, onColumnClick, onScrollAreaEnter, onScrollAreaLeave, selectedColumns = [] } = data;
|
||||
|
||||
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 - 숨김 처리 */}
|
||||
<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" />
|
||||
|
||||
{/* 테이블 헤더 - 통일된 디자인 */}
|
||||
<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>
|
||||
{table.description && <p className="mt-1 truncate text-xs opacity-75">{table.description}</p>}
|
||||
</div>
|
||||
|
|
@ -56,7 +56,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
<div
|
||||
key={columnKey}
|
||||
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)}
|
||||
>
|
||||
|
|
@ -64,9 +64,9 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono font-medium">{columnDisplayName}</span>
|
||||
<span className="text-gray-500">{columnType}</span>
|
||||
<span className="text-muted-foreground">{columnType}</span>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<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}>
|
||||
{loading ? "로딩중..." : "새로고침"}
|
||||
</Button>
|
||||
|
|
@ -82,7 +82,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
|
||||
{/* 검색 입력 */}
|
||||
<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
|
||||
placeholder="테이블명으로 검색..."
|
||||
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">
|
||||
{loading ? (
|
||||
<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>
|
||||
) : filteredTables.length === 0 ? (
|
||||
<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 ? "검색 결과가 없습니다." : "등록된 테이블이 없습니다."}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -114,14 +114,14 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
<Card
|
||||
key={table.tableName}
|
||||
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)}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<div>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
|
|
@ -132,7 +132,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
{isSelected && <span className="font-medium text-primary">(추가됨)</span>}
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -142,7 +142,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
</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">
|
||||
<span>전체 테이블: {tables.length}개</span>
|
||||
{searchTerm && <span>검색 결과: {filteredTables.length}개</span>}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
|||
return (
|
||||
<div className="space-y-2">
|
||||
{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 />
|
||||
조건이 없으면 항상 실행됩니다.
|
||||
|
|
@ -92,7 +92,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
|||
)}
|
||||
{/* 그룹 레벨에 따른 들여쓰기 */}
|
||||
<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` }}
|
||||
>
|
||||
<span className="font-mono text-sm text-primary">(</span>
|
||||
|
|
@ -110,7 +110,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
|||
return (
|
||||
<div key={condition.id} className="flex items-center gap-2">
|
||||
<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` }}
|
||||
>
|
||||
<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"}
|
||||
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 />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -124,11 +124,11 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
|
|||
</Select>
|
||||
)}
|
||||
<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` }}
|
||||
>
|
||||
<span className="font-mono text-xs text-green-600">(</span>
|
||||
<span className="text-xs text-green-600">그룹 시작</span>
|
||||
<span className="font-mono text-xs text-emerald-600">(</span>
|
||||
<span className="text-xs text-emerald-600">그룹 시작</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
|
@ -147,11 +147,11 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
|
|||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<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` }}
|
||||
>
|
||||
<span className="font-mono text-xs text-green-600">)</span>
|
||||
<span className="text-xs text-green-600">그룹 끝</span>
|
||||
<span className="font-mono text-xs text-emerald-600">)</span>
|
||||
<span className="text-xs text-emerald-600">그룹 끝</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
|
@ -174,7 +174,7 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
|
|||
value={settings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
|
||||
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 />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
|
|||
|
|
@ -82,21 +82,21 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
|
|||
<div className="mt-3">
|
||||
<details className="group">
|
||||
<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
|
||||
? "border-red-300 bg-destructive/10 text-red-700"
|
||||
: "border-gray-200 text-gray-700"
|
||||
? "border-destructive/30 bg-destructive/10 text-destructive"
|
||||
: "border-border text-foreground"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
🔍 이 액션의 실행 조건
|
||||
{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 && (
|
||||
<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}개
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -113,14 +113,14 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
|
|||
e.stopPropagation();
|
||||
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="조건 모두 삭제"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</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="flex gap-1">
|
||||
<Button size="sm" variant="outline" onClick={addActionCondition} className="h-6 text-xs">
|
||||
|
|
@ -151,13 +151,13 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
|
|||
<div
|
||||
className={`rounded border p-3 text-xs ${
|
||||
isConditionRequired
|
||||
? "border-destructive/20 bg-destructive/10 text-red-700"
|
||||
: "border-gray-200 bg-gray-50 text-muted-foreground"
|
||||
? "border-destructive/20 bg-destructive/10 text-destructive"
|
||||
: "border-border bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{isConditionRequired ? (
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-red-500">⚠️</span>
|
||||
<span className="text-destructive">⚠️</span>
|
||||
<div>
|
||||
<div className="font-medium">실행조건이 필요합니다</div>
|
||||
<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