UI 수정:화면 수정

This commit is contained in:
2025-12-11 20:07:19 +09:00
parent b906ec1851
commit 7d15c9be7c
26 changed files with 2629 additions and 557 deletions

View File

@@ -431,7 +431,7 @@ export function CategoryEvaluationCard({
/>
{/* 보은군 평균 - Green */}
<Radar
name="보은군"
name="보은군 평균"
dataKey="regionVal"
stroke="#10b981"
fill="#10b981"
@@ -441,7 +441,7 @@ export function CategoryEvaluationCard({
/>
{/* 농가 평균 - Navy Blue (중간) */}
<Radar
name="농가"
name="농가 평균"
dataKey="farmVal"
stroke="#1F3A8F"
fill="#1F3A8F"
@@ -480,14 +480,14 @@ export function CategoryEvaluationCard({
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded" style={{ backgroundColor: '#10b981' }}></span>
<span className="text-slate-300"></span>
<span className="text-slate-300"> </span>
</span>
<span className="text-white font-semibold">{regionVal > 0 ? '+' : ''}{regionVal.toFixed(2)}σ</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded" style={{ backgroundColor: '#1F3A8F' }}></span>
<span className="text-slate-300"></span>
<span className="text-slate-300"> </span>
</span>
<span className="text-white font-semibold">{farmVal > 0 ? '+' : ''}{farmVal.toFixed(2)}σ</span>
</div>
@@ -513,11 +513,11 @@ export function CategoryEvaluationCard({
<div className="flex items-center justify-center gap-5 sm:gap-8 py-3 border-t border-border">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded" style={{ backgroundColor: '#10b981' }}></div>
<span className="text-base text-muted-foreground"></span>
<span className="text-base text-muted-foreground"> </span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded" style={{ backgroundColor: '#1F3A8F' }}></div>
<span className="text-base text-muted-foreground"></span>
<span className="text-base text-muted-foreground"> </span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded" style={{ backgroundColor: '#1482B0' }}></div>

View File

@@ -982,28 +982,35 @@ export function NormalDistributionChart({
{/* 농가 vs 개체 거리 화살표 - 농가 배지 바로 아래 */}
{Math.abs(cowVsFarm) > 0.05 && (
<g>
{/* 막대선: 내 개체선 전까지만 */}
<line
x1={cowVsFarm >= 0 ? farmX + 5 : farmX - 5}
y1={farmArrowY}
x2={cowVsFarm >= 0 ? cowX : cowX}
y2={farmArrowY}
stroke={cowVsFarm >= 0 ? '#22c55e' : '#ef4444'}
strokeWidth={isMobile ? 3 : 4}
/>
{/* 화살표 머리: 내 개체선 넘어서 (뾰족한 화살표) */}
{/* 화살표 머리 및 막대선 */}
{(() => {
const arrowBaseOffset = isMobile ? 10 : 16
const arrowTipOffset = isMobile ? 10 : 16
const arrowHeight = isMobile ? 8 : 12
// 화살표 머리 시작점 (막대기 끝점)
const arrowHeadStart = cowVsFarm >= 0
? cowX - arrowBaseOffset - arrowTipOffset
: cowX + arrowBaseOffset + arrowTipOffset
return (
<polygon
points={cowVsFarm >= 0
? `${cowX - arrowBaseOffset},${farmArrowY - arrowHeight} ${cowX - arrowBaseOffset},${farmArrowY + arrowHeight} ${cowX + arrowTipOffset},${farmArrowY}`
: `${cowX + arrowBaseOffset},${farmArrowY - arrowHeight} ${cowX + arrowBaseOffset},${farmArrowY + arrowHeight} ${cowX - arrowTipOffset},${farmArrowY}`
}
fill={cowVsFarm >= 0 ? '#22c55e' : '#ef4444'}
/>
<>
{/* 막대선: 화살표 머리 시작점까지만 */}
<line
x1={cowVsFarm >= 0 ? farmX + 5 : farmX - 5}
y1={farmArrowY}
x2={arrowHeadStart}
y2={farmArrowY}
stroke={cowVsFarm >= 0 ? '#22c55e' : '#ef4444'}
strokeWidth={isMobile ? 3 : 4}
/>
{/* 화살표 머리: 내 개체선에 딱 맞게 */}
<polygon
points={cowVsFarm >= 0
? `${arrowHeadStart},${farmArrowY - arrowHeight} ${arrowHeadStart},${farmArrowY + arrowHeight} ${cowX},${farmArrowY}`
: `${arrowHeadStart},${farmArrowY - arrowHeight} ${arrowHeadStart},${farmArrowY + arrowHeight} ${cowX},${farmArrowY}`
}
fill={cowVsFarm >= 0 ? '#22c55e' : '#ef4444'}
/>
</>
)
})()}
<rect
@@ -1030,28 +1037,35 @@ export function NormalDistributionChart({
{/* 보은군 vs 개체 거리 화살표 - 보은군 배지 바로 아래 */}
{Math.abs(cowVsRegion) > 0.05 && (
<g>
{/* 막대선: 내 개체선 전까지만 */}
<line
x1={cowVsRegion >= 0 ? regionX + 5 : regionX - 5}
y1={regionArrowY}
x2={cowVsRegion >= 0 ? cowX : cowX}
y2={regionArrowY}
stroke={cowVsRegion >= 0 ? '#22c55e' : '#ef4444'}
strokeWidth={isMobile ? 3 : 4}
/>
{/* 화살표 머리: 내 개체선 넘어서 (뾰족한 화살표) */}
{/* 화살표 머리 및 막대선 */}
{(() => {
const arrowBaseOffset = isMobile ? 10 : 16
const arrowTipOffset = isMobile ? 10 : 16
const arrowHeight = isMobile ? 8 : 12
// 화살표 머리 시작점 (막대기 끝점)
const arrowHeadStart = cowVsRegion >= 0
? cowX - arrowBaseOffset - arrowTipOffset
: cowX + arrowBaseOffset + arrowTipOffset
return (
<polygon
points={cowVsRegion >= 0
? `${cowX - arrowBaseOffset},${regionArrowY - arrowHeight} ${cowX - arrowBaseOffset},${regionArrowY + arrowHeight} ${cowX + arrowTipOffset},${regionArrowY}`
: `${cowX + arrowBaseOffset},${regionArrowY - arrowHeight} ${cowX + arrowBaseOffset},${regionArrowY + arrowHeight} ${cowX - arrowTipOffset},${regionArrowY}`
}
fill={cowVsRegion >= 0 ? '#22c55e' : '#ef4444'}
/>
<>
{/* 막대선: 화살표 머리 시작점까지만 */}
<line
x1={cowVsRegion >= 0 ? regionX + 5 : regionX - 5}
y1={regionArrowY}
x2={arrowHeadStart}
y2={regionArrowY}
stroke={cowVsRegion >= 0 ? '#22c55e' : '#ef4444'}
strokeWidth={isMobile ? 3 : 4}
/>
{/* 화살표 머리: 내 개체선에 딱 맞게 */}
<polygon
points={cowVsRegion >= 0
? `${arrowHeadStart},${regionArrowY - arrowHeight} ${arrowHeadStart},${regionArrowY + arrowHeight} ${cowX},${regionArrowY}`
: `${arrowHeadStart},${regionArrowY - arrowHeight} ${arrowHeadStart},${regionArrowY + arrowHeight} ${cowX},${regionArrowY}`
}
fill={cowVsRegion >= 0 ? '#22c55e' : '#ef4444'}
/>
</>
)
})()}
<rect

View File

@@ -305,14 +305,15 @@ export default function CowOverviewPage() {
'우둔rate', '설도rate', '사태rate', '양지rate', '갈비rate',
]
const traitConditions = Object.entries(filters.traitWeights)
.filter(([, weight]) => weight > 0)
.map(([traitNm, weight]) => ({ traitNm, weight }))
const finalConditions = traitConditions.length > 0
? traitConditions
// 필터가 활성화되고 형질이 선택되어 있으면 가중치 조건 생성 (대시보드와 동일 로직)
const finalConditions = filters.isActive && filters.selectedTraits && filters.selectedTraits.length > 0
? filters.selectedTraits.map(traitNm => ({
traitNm,
weight: (filters.traitWeights as Record<string, number>)[traitNm] || 1
}))
: ALL_TRAITS.map(traitNm => ({ traitNm, weight: 1 }))
const indexResult = await genomeApi.getSelectionIndex(cowNo, finalConditions)
setSelectionIndex(indexResult)
} catch (compErr) {
@@ -332,7 +333,7 @@ export default function CowOverviewPage() {
}
fetchData()
}, [cowNo, toast, filters.traitWeights])
}, [cowNo, toast, filters.isActive, filters.selectedTraits, filters.traitWeights])
// API 데이터를 화면용으로 변환
const GENOMIC_TRAITS = useMemo(() => {

View File

@@ -56,6 +56,7 @@ interface CowWithGenes extends Cow {
cowShortNo?: string // 개체 요약번호
cowReproType?: string // 번식 타입
anlysDt?: string // 분석일자
unavailableReason?: string // 분석불가 사유 (부불일치, 모불일치, 모이력제부재 등)
}
function MyCowContent() {
@@ -266,7 +267,7 @@ function MyCowContent() {
// response는 { items: RankingResultItem[], total, criteriaType, timestamp } 형식
// items의 각 요소는 { entity, rank, sortValue, grade, details } 형식
interface RankingItem {
entity: Cow & { genes?: Record<string, number>; calvingCount?: number; bcs?: number; inseminationCount?: number; inbreedingPercent?: number; sireKpn?: string; anlysDt?: string };
entity: Cow & { genes?: Record<string, number>; calvingCount?: number; bcs?: number; inseminationCount?: number; inbreedingPercent?: number; sireKpn?: string; anlysDt?: string; unavailableReason?: string };
rank: number;
sortValue: number;
grade: string;
@@ -351,6 +352,8 @@ function MyCowContent() {
sireKpn: item.entity.sireKpn ?? null,
// 분석일자
anlysDt: item.entity.anlysDt ?? null,
// 분석불가 사유
unavailableReason: item.entity.unavailableReason ?? null,
//====================================================================================================================
// 형질 데이터 (백엔드에서 계산됨, 형질명 → 표준화육종가 매핑)
// 백엔드 응답: { traitName: string, traitVal: number, traitEbv: number, traitPercentile: number }
@@ -982,7 +985,11 @@ function MyCowContent() {
year: '2-digit',
month: '2-digit',
day: '2-digit'
}) : '-'}
}) : (
<span className={cow.unavailableReason ? 'text-red-500 font-medium' : 'text-slate-400'}>
{cow.unavailableReason || '미분석'}
</span>
)}
</td>
<td className="cow-table-cell border-r-2 border-r-gray-300 !py-2 !px-0.5">
{(cow.genomeScore !== undefined && cow.genomeScore !== null) ? (
@@ -1140,9 +1147,15 @@ function MyCowContent() {
</Badge>
</div>
<div className="flex-shrink-0 ml-2">
<span className="font-bold text-xl text-primary">
{cow.genomeScore !== undefined && cow.genomeScore !== null ? cow.genomeScore.toFixed(2) : '-'}
</span>
{cow.genomeScore !== undefined && cow.genomeScore !== null ? (
<span className="font-bold text-xl text-primary">
{cow.genomeScore.toFixed(2)}
</span>
) : (
<Badge className="text-[11px] px-1.5 py-0.5 bg-slate-500 text-white border-0 font-medium">
</Badge>
)}
</div>
</div>
@@ -1179,7 +1192,11 @@ function MyCowContent() {
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">
{cow.anlysDt ? new Date(cow.anlysDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : '-'}
{cow.anlysDt ? new Date(cow.anlysDt).toLocaleDateString('ko-KR', { year: '2-digit', month: 'numeric', day: 'numeric' }) : (
<span className={cow.unavailableReason ? 'text-red-500' : 'text-slate-400'}>
{cow.unavailableReason || '미분석'}
</span>
)}
</span>
</div>
</div>

View File

@@ -145,10 +145,17 @@ export default function DashboardPage() {
useEffect(() => {
const fetchStats = async () => {
if (!farmNo) return
// 필터가 활성화되고 형질이 선택되어 있으면 가중치 조건 생성
const traitConditions = filters.isActive && filters.selectedTraits && filters.selectedTraits.length > 0
? filters.selectedTraits.map(traitNm => ({
traitNm,
weight: (filters.traitWeights as Record<string, number>)[traitNm] || 1
}))
: undefined
try {
const [statsData, rankingData] = await Promise.all([
genomeApi.getDashboardStats(farmNo),
genomeApi.getFarmRegionRanking(farmNo)
genomeApi.getFarmRegionRanking(farmNo, traitConditions)
])
setStats(statsData)
setFarmRanking(rankingData)
@@ -157,7 +164,7 @@ export default function DashboardPage() {
}
}
fetchStats()
}, [farmNo])
}, [farmNo, filters.isActive, filters.selectedTraits, filters.traitWeights])
// 연도별 형질 추이 데이터 로드
useEffect(() => {
@@ -268,9 +275,10 @@ export default function DashboardPage() {
// 개별 형질 선택 시 - traitAverages에서 해당 형질 찾기
const traitData = stats?.traitAverages?.find(t => t.traitName === distributionBasis)
if (traitData) {
farmScore = traitData.avgEbv ?? 0
// 보은군 평균은 0으로 가정 (표준화 육종가 기준)
regionScore = 0
const farmEpd = traitData.avgEpd ?? 0
const regionEpd = traitData.regionAvgEpd ?? 0
farmScore = farmEpd - regionEpd // 보은군 대비 차이
regionScore = 0 // 보은군 = 기준점 (0)
label = distributionBasis
rank = traitData.rank ?? null
totalFarms = traitData.totalFarms ?? farmRanking.totalFarmsInRegion ?? 0
@@ -278,28 +286,60 @@ export default function DashboardPage() {
}
}
const absMax = Math.max(Math.abs(farmScore), Math.abs(regionScore))
const range = Math.max(2.5, Math.ceil(absMax * 2) / 2 + 0.5)
const absMax = Math.max(Math.abs(farmScore), 0.1)
// 데이터 범위에 따라 적절한 X축 범위 계산
// absMax의 1.5배를 범위로 하고, 깔끔한 숫자로 반올림
const rawRange = absMax * 1.5
let range: number
let step: number
// 정규분포 비율 (0.5σ 단위)
const sigmaPercentMap: Record<string, number> = {
'-3.0': 0.10, '-2.5': 0.52, '-2.0': 1.65, '-1.5': 4.41,
'-1.0': 9.18, '-0.5': 15.00, '0.0': 19.14, '0.5': 19.14,
'1.0': 15.00, '1.5': 9.18, '2.0': 4.41, '2.5': 1.65, '3.0': 0.52,
if (rawRange <= 3) {
// 표준화 육종가 스케일 (전체 선발지수)
range = Math.max(2.5, Math.ceil(rawRange * 2) / 2)
step = 0.5
} else if (rawRange <= 10) {
range = Math.ceil(rawRange / 2) * 2 // 2 단위로 반올림
step = range / 5
} else if (rawRange <= 50) {
range = Math.ceil(rawRange / 10) * 10 // 10 단위로 반올림
step = range / 5
} else {
range = Math.ceil(rawRange / 20) * 20 // 20 단위로 반올림
step = range / 5
}
// 정규분포 비율 계산 (구간 개수에 맞춤)
const numBins = Math.round((range * 2) / step)
const bins = []
for (let sigma = -range; sigma < range; sigma += 0.5) {
const key = sigma.toFixed(1)
const percent = sigmaPercentMap[key] ?? 0.1
const total = farmRanking.totalFarmsInRegion || 50
// 정규분포 PDF 기반으로 각 구간의 비율 계산
const normalPDF = (x: number, sigma: number = range / 3) => {
return Math.exp(-0.5 * Math.pow(x / sigma, 2)) / (sigma * Math.sqrt(2 * Math.PI))
}
const total = farmRanking.totalFarmsInRegion || 50
let totalPercent = 0
const tempBins = []
for (let i = 0; i < numBins; i++) {
const min = -range + i * step
const max = min + step
const midPoint = (min + max) / 2
const percent = normalPDF(midPoint) * step * 100
totalPercent += percent
tempBins.push({ min, max, midPoint, percent })
}
// 비율 정규화
for (const bin of tempBins) {
const normalizedPercent = (bin.percent / totalPercent) * 100
bins.push({
range: `${sigma}σ~${sigma + 0.5}σ`,
min: sigma,
max: sigma + 0.5,
midPoint: sigma + 0.25,
count: Math.round(total * percent / 100),
percent
range: `${bin.min.toFixed(1)}~${bin.max.toFixed(1)}`,
min: bin.min,
max: bin.max,
midPoint: bin.midPoint,
count: Math.round(total * normalizedPercent / 100),
percent: normalizedPercent
})
}
@@ -636,22 +676,27 @@ export default function DashboardPage() {
? (actualLineLength < minLineLength ? farmX - minLineLength : regionX + 5)
: (actualLineLength < minLineLength ? farmX + minLineLength : regionX - 5)
// 화살표 머리 시작점 (막대기 끝점)
const arrowHeadStart = diff >= 0
? farmX - arrowBaseOffset - arrowTipOffset
: farmX + arrowBaseOffset + arrowTipOffset
return (
<g>
{/* 막대선: 최소 길이 보장 */}
{/* 막대선: 화살표 머리 시작점까지만 */}
<line
x1={lineStartX}
y1={arrowY}
x2={farmX}
x2={arrowHeadStart}
y2={arrowY}
stroke={color}
strokeWidth={isMobileView ? 3 : 4}
/>
{/* 화살표 머리 (농가선 넘어서, 뾰족한 화살표) */}
{/* 화살표 머리 (농가선에 딱 맞게) */}
<polygon
points={diff >= 0
? `${farmX - arrowBaseOffset},${arrowY - arrowHeight} ${farmX - arrowBaseOffset},${arrowY + arrowHeight} ${farmX + arrowTipOffset},${arrowY}`
: `${farmX + arrowBaseOffset},${arrowY - arrowHeight} ${farmX + arrowBaseOffset},${arrowY + arrowHeight} ${farmX - arrowTipOffset},${arrowY}`
? `${arrowHeadStart},${arrowY - arrowHeight} ${arrowHeadStart},${arrowY + arrowHeight} ${farmX},${arrowY}`
: `${arrowHeadStart},${arrowY - arrowHeight} ${arrowHeadStart},${arrowY + arrowHeight} ${farmX},${arrowY}`
}
fill={color}
/>
@@ -1169,12 +1214,13 @@ export default function DashboardPage() {
{/* 우측: 상세 바 차트 - 68% */}
<div className="w-full lg:flex-1 space-y-3 sm:space-y-3">
{categoryData.map(({ category, avgEpd, avgPercentile, traitCount }) => {
{categoryData.map(({ category, avgEpd, avgPercentile, traitCount }, index) => {
const isPositive = avgEpd >= 0
// 바 너비: 육종가 스케일에 맞게 계산
const barWidth = Math.min(Math.abs(avgEpd) / maxAbs * 45, 48)
const isFirst = index === 0
return (
<div key={category} className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<div key={category} className={`flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 ${isFirst ? 'sm:mt-4' : ''}`}>
{/* 모바일: 카테고리명 + 값을 한 줄에 */}
<div className="flex items-center justify-between sm:hidden">
<span className="text-sm font-bold text-slate-700">{category}</span>
@@ -1201,15 +1247,22 @@ export default function DashboardPage() {
</div>
{/* 데스크톱: 한 줄 레이아웃 */}
<span className="hidden sm:block w-12 text-base font-bold text-slate-700 flex-shrink-0">{category}</span>
<div className="hidden sm:block w-[70%] h-10 bg-slate-100 rounded-full relative overflow-hidden">
<div className="absolute left-1/2 top-0 bottom-0 w-0.5 bg-slate-400 z-10"></div>
<div
className={`absolute top-1 bottom-1 rounded-full transition-all duration-500 ${isPositive ? 'bg-[#1F3A8F]' : 'bg-red-400'}`}
style={{
left: isPositive ? '50%' : `${50 - barWidth}%`,
width: `${barWidth}%`,
}}
/>
<div className={`hidden sm:block w-[70%] h-10 relative ${isFirst ? 'mt-5' : ''}`}>
{/* 보은군 평균 라벨 (첫 번째 바 위에만 표시) */}
{isFirst && (
<span className="absolute left-1/2 -translate-x-1/2 -top-5 text-[11px] text-slate-500 font-medium whitespace-nowrap"> </span>
)}
{/* 바 차트 */}
<div className="absolute inset-0 bg-slate-100 rounded-full overflow-hidden">
<div className="absolute left-1/2 top-0 bottom-0 w-0.5 bg-slate-400 z-10"></div>
<div
className={`absolute top-1 bottom-1 rounded-full transition-all duration-500 ${isPositive ? 'bg-[#1F3A8F]' : 'bg-red-400'}`}
style={{
left: isPositive ? '50%' : `${50 - barWidth}%`,
width: `${barWidth}%`,
}}
/>
</div>
</div>
<span className={`hidden sm:block w-20 text-right text-lg font-bold flex-shrink-0 ml-3 ${isPositive ? 'text-[#1F3A8F]' : 'text-red-500'}`}>
{isPositive ? '+' : ''}{avgEpd.toFixed(2)}

View File

@@ -0,0 +1,237 @@
'use client';
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import Image from "next/image";
import { User, Mail, ArrowLeft } from "lucide-react";
// 시안 1: 현재 디자인
function FindIdDesign1() {
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input placeholder="홍길동" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input type="email" placeholder="example@email.com" />
<p className="text-xs text-gray-500"> </p>
</div>
<Button className="w-full"> </Button>
<Button variant="outline" className="w-full border-2 border-primary text-primary"></Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center"><span className="w-full border-t" /></div>
<div className="relative flex justify-center text-xs"><span className="bg-white px-2 text-gray-500"> ?</span></div>
</div>
<Button variant="outline" className="w-full border-2 border-primary text-primary"></Button>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 2: 아이콘 + 간결한 레이아웃
function FindIdDesign2() {
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input placeholder="이름을 입력하세요" className="pl-10 h-11" />
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input type="email" placeholder="이메일을 입력하세요" className="pl-10 h-11" />
</div>
</div>
<Button className="w-full h-11"> </Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center"><span className="w-full border-t" /></div>
<div className="relative flex justify-center text-xs"><span className="bg-white px-2 text-gray-500"></span></div>
</div>
<Button variant="outline" className="w-full h-11 border-2 border-primary text-primary"> </Button>
<div className="text-center">
<a href="#" className="text-sm text-gray-500 hover:text-primary"> ?</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 3: 뒤로가기 버튼 + 깔끔한 구조
function FindIdDesign3() {
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<a href="#" className="flex items-center gap-1 text-sm text-gray-500 hover:text-primary mb-2">
<ArrowLeft className="w-4 h-4" />
</a>
<div className="flex flex-col gap-1 mb-2">
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input placeholder="이름을 입력하세요" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input type="email" placeholder="이메일을 입력하세요" className="h-11" />
</div>
<Button className="w-full h-11 mt-2"> </Button>
<div className="flex items-center justify-center gap-4 text-sm text-gray-500 mt-2">
<a href="#" className="hover:text-primary"> </a>
<span>|</span>
<a href="#" className="hover:text-primary"></a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 4: 로그인과 통일된 스타일 (추천)
function FindIdDesign4() {
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input placeholder="이름을 입력하세요" className="h-11" />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium"></label>
<a href="#" className="text-xs text-primary hover:underline"> </a>
</div>
<Input type="email" placeholder="이메일을 입력하세요" className="h-11" />
</div>
<Button className="w-full h-11"> </Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center"><span className="w-full border-t" /></div>
<div className="relative flex justify-center text-xs"><span className="bg-white px-2 text-gray-500"></span></div>
</div>
<Button variant="outline" className="w-full h-11 border-2 border-primary text-primary"></Button>
<div className="text-center">
<a href="#" className="text-sm text-gray-500 hover:text-primary"> ? </a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default function FindIdDemo() {
const designs = [
{ id: "current", name: "현재", description: "현재 적용된 디자인", features: ["기존 레이아웃"], component: FindIdDesign1 },
{ id: "icon", name: "시안 2", description: "아이콘 + 간결한 레이아웃", features: ["입력 필드 아이콘", "간결한 하단 링크"], component: FindIdDesign2 },
{ id: "back", name: "시안 3", description: "뒤로가기 버튼 + 좌측 정렬 제목", features: ["뒤로가기 버튼", "좌측 정렬"], component: FindIdDesign3 },
{ id: "unified", name: "시안 4", description: "로그인과 통일된 스타일 (추천)", features: ["로그인 스타일 통일", "비밀번호 찾기 위치"], component: FindIdDesign4 },
];
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="text-gray-600 mt-2"> </p>
</div>
<Tabs defaultValue="current" className="space-y-6">
<TabsList className="grid grid-cols-4 w-full h-auto p-1">
{designs.map((design) => (
<TabsTrigger key={design.id} value={design.id} className="py-3 text-xs sm:text-sm data-[state=active]:bg-primary data-[state=active]:text-white">
{design.name}
</TabsTrigger>
))}
</TabsList>
{designs.map((design) => (
<TabsContent key={design.id} value={design.id} className="space-y-4">
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h2 className="text-lg font-semibold">{design.name}: {design.description}</h2>
</div>
<div className="flex gap-2 flex-wrap">
{design.features.map((feature, idx) => (
<Badge key={idx} variant="secondary">{feature}</Badge>
))}
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<design.component />
</div>
</TabsContent>
))}
</Tabs>
</div>
</div>
);
}

View File

@@ -0,0 +1,237 @@
'use client';
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import Image from "next/image";
import { User, Mail, ArrowLeft, Eye, EyeOff } from "lucide-react";
// 시안 1: 현재 디자인
function FindPwDesign1() {
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input placeholder="아이디를 입력하세요" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input type="email" placeholder="이메일 주소를 입력해주세요" />
<p className="text-xs text-gray-500"> </p>
</div>
<Button className="w-full"> </Button>
<Button variant="outline" className="w-full border-2 border-primary text-primary"></Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center"><span className="w-full border-t" /></div>
<div className="relative flex justify-center text-xs"><span className="bg-white px-2 text-gray-500"> ?</span></div>
</div>
<Button variant="outline" className="w-full border-2 border-primary text-primary"></Button>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 2: 아이콘 + 간결한 레이아웃
function FindPwDesign2() {
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input placeholder="아이디를 입력하세요" className="pl-10 h-11" />
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input type="email" placeholder="이메일을 입력하세요" className="pl-10 h-11" />
</div>
</div>
<Button className="w-full h-11"> </Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center"><span className="w-full border-t" /></div>
<div className="relative flex justify-center text-xs"><span className="bg-white px-2 text-gray-500"></span></div>
</div>
<Button variant="outline" className="w-full h-11 border-2 border-primary text-primary"> </Button>
<div className="text-center">
<a href="#" className="text-sm text-gray-500 hover:text-primary"> ?</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 3: 뒤로가기 버튼 + 좌측 정렬
function FindPwDesign3() {
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<a href="#" className="flex items-center gap-1 text-sm text-gray-500 hover:text-primary mb-2">
<ArrowLeft className="w-4 h-4" />
</a>
<div className="flex flex-col gap-1 mb-2">
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input placeholder="아이디를 입력하세요" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input type="email" placeholder="이메일을 입력하세요" className="h-11" />
</div>
<Button className="w-full h-11 mt-2"> </Button>
<div className="flex items-center justify-center gap-4 text-sm text-gray-500 mt-2">
<a href="#" className="hover:text-primary"> </a>
<span>|</span>
<a href="#" className="hover:text-primary"></a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 4: 로그인과 통일된 스타일 (추천)
function FindPwDesign4() {
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"> </h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input placeholder="아이디를 입력하세요" className="h-11" />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium"></label>
<a href="#" className="text-xs text-primary hover:underline"> </a>
</div>
<Input type="email" placeholder="이메일을 입력하세요" className="h-11" />
</div>
<Button className="w-full h-11"> </Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center"><span className="w-full border-t" /></div>
<div className="relative flex justify-center text-xs"><span className="bg-white px-2 text-gray-500"></span></div>
</div>
<Button variant="outline" className="w-full h-11 border-2 border-primary text-primary"></Button>
<div className="text-center">
<a href="#" className="text-sm text-gray-500 hover:text-primary"> ? </a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default function FindPwDemo() {
const designs = [
{ id: "current", name: "현재", description: "현재 적용된 디자인", features: ["기존 레이아웃"], component: FindPwDesign1 },
{ id: "icon", name: "시안 2", description: "아이콘 + 간결한 레이아웃", features: ["입력 필드 아이콘", "간결한 하단 링크"], component: FindPwDesign2 },
{ id: "back", name: "시안 3", description: "뒤로가기 버튼 + 좌측 정렬 제목", features: ["뒤로가기 버튼", "좌측 정렬"], component: FindPwDesign3 },
{ id: "unified", name: "시안 4", description: "로그인과 통일된 스타일 (추천)", features: ["로그인 스타일 통일", "아이디 찾기 위치"], component: FindPwDesign4 },
];
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="text-gray-600 mt-2"> </p>
</div>
<Tabs defaultValue="current" className="space-y-6">
<TabsList className="grid grid-cols-4 w-full h-auto p-1">
{designs.map((design) => (
<TabsTrigger key={design.id} value={design.id} className="py-3 text-xs sm:text-sm data-[state=active]:bg-primary data-[state=active]:text-white">
{design.name}
</TabsTrigger>
))}
</TabsList>
{designs.map((design) => (
<TabsContent key={design.id} value={design.id} className="space-y-4">
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h2 className="text-lg font-semibold">{design.name}: {design.description}</h2>
</div>
<div className="flex gap-2 flex-wrap">
{design.features.map((feature, idx) => (
<Badge key={idx} variant="secondary">{feature}</Badge>
))}
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<design.component />
</div>
</TabsContent>
))}
</Tabs>
</div>
</div>
);
}

View File

@@ -0,0 +1,549 @@
'use client';
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import Image from "next/image";
import { LogIn, Eye, EyeOff, User, Lock } from "lucide-react";
// 시안 1: 현재 디자인
function LoginDesign1() {
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image
src="/logo-graphic.svg"
alt="로고"
fill
className="object-contain p-16"
priority
/>
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input placeholder="아이디를 입력하세요" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input type="password" placeholder="비밀번호를 입력하세요" />
</div>
<Button className="w-full"></Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-muted-foreground"> ?</span>
</div>
</div>
<Button variant="outline" className="w-full border-2 border-primary text-primary">
</Button>
<div className="text-center text-sm">
<a href="#" className="hover:underline"> </a>
{" | "}
<a href="#" className="hover:underline"> </a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 2: 현재 + 비밀번호 토글 + 아이콘
function LoginDesign2() {
const [showPassword, setShowPassword] = useState(false);
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image
src="/logo-graphic.svg"
alt="로고"
fill
className="object-contain p-16"
priority
/>
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input placeholder="아이디를 입력하세요" className="pl-10" />
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input
type={showPassword ? "text" : "password"}
placeholder="비밀번호를 입력하세요"
className="pl-10 pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<Button className="w-full">
<LogIn className="w-4 h-4 mr-2" />
</Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-muted-foreground"> ?</span>
</div>
</div>
<Button variant="outline" className="w-full border-2 border-primary text-primary">
</Button>
<div className="text-center text-sm">
<a href="#" className="hover:underline"> </a>
{" | "}
<a href="#" className="hover:underline"> </a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 3: 현재 + 더 큰 입력 필드 + 부드러운 그림자
function LoginDesign3() {
const [showPassword, setShowPassword] = useState(false);
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-gradient-to-br from-slate-50 to-slate-100 relative hidden lg:flex items-center justify-center">
<Image
src="/logo-graphic.svg"
alt="로고"
fill
className="object-contain p-16"
priority
/>
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-12">
<div className="w-full max-w-[360px]">
<div className="flex flex-col gap-5">
<div className="flex flex-col items-center gap-2 text-center mb-4">
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"></label>
<Input
placeholder="아이디를 입력하세요"
className="h-12 text-base shadow-sm border-gray-200 focus:border-primary focus:ring-primary"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"></label>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
placeholder="비밀번호를 입력하세요"
className="h-12 text-base pr-10 shadow-sm border-gray-200 focus:border-primary focus:ring-primary"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<Button className="w-full h-12 text-base shadow-md hover:shadow-lg transition-shadow">
</Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-gray-200" />
</div>
<div className="relative flex justify-center text-xs">
<span className="bg-white px-3 text-gray-500"> ?</span>
</div>
</div>
<Button variant="outline" className="w-full h-12 text-base border-2 border-primary text-primary hover:bg-primary hover:text-white transition-colors">
</Button>
<div className="text-center text-sm text-gray-500">
<a href="#" className="hover:text-primary transition-colors"> </a>
<span className="mx-2">|</span>
<a href="#" className="hover:text-primary transition-colors"> </a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 4: 현재 + 아이디 저장 + 간결한 링크
function LoginDesign4() {
const [showPassword, setShowPassword] = useState(false);
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image
src="/logo-graphic.svg"
alt="로고"
fill
className="object-contain p-16"
priority
/>
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input placeholder="아이디를 입력하세요" className="h-11" />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium"></label>
<a href="#" className="text-xs text-primary hover:underline"> </a>
</div>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
placeholder="비밀번호를 입력하세요"
className="h-11 pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" className="rounded border-gray-300 text-primary focus:ring-primary" />
<span className="text-sm text-gray-600"> </span>
</label>
<Button className="w-full h-11"></Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-muted-foreground"></span>
</div>
</div>
<Button variant="outline" className="w-full h-11 border-2 border-primary text-primary">
</Button>
<div className="text-center">
<a href="#" className="text-sm text-gray-500 hover:text-primary"> ?</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 5: 현재 + 컬러 강조 배경
function LoginDesign5() {
const [showPassword, setShowPassword] = useState(false);
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-primary/5 relative hidden lg:flex items-center justify-center">
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-primary/5" />
<Image
src="/logo-graphic.svg"
alt="로고"
fill
className="object-contain p-16"
priority
/>
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold text-primary"></h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-primary/60" />
<Input
placeholder="아이디를 입력하세요"
className="pl-10 h-11 border-primary/20 focus:border-primary"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-primary/60" />
<Input
type={showPassword ? "text" : "password"}
placeholder="비밀번호를 입력하세요"
className="pl-10 pr-10 h-11 border-primary/20 focus:border-primary"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-primary"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<Button className="w-full h-11 bg-primary hover:bg-primary/90">
<LogIn className="w-4 h-4 mr-2" />
</Button>
<div className="flex items-center justify-between text-sm">
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" className="rounded border-primary/30 text-primary focus:ring-primary" />
<span className="text-gray-600"> </span>
</label>
<a href="#" className="text-primary hover:underline"> </a>
</div>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-primary/20" />
</div>
<div className="relative flex justify-center text-xs">
<span className="bg-white px-2 text-gray-500"> ?</span>
</div>
</div>
<Button variant="outline" className="w-full h-11 border-2 border-primary text-primary hover:bg-primary/5">
</Button>
<div className="text-center">
<a href="#" className="text-sm text-gray-500 hover:text-primary"> </a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 6: 현재 + 라운드 스타일
function LoginDesign6() {
const [showPassword, setShowPassword] = useState(false);
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image
src="/logo-graphic.svg"
alt="로고"
fill
className="object-contain p-16"
priority
/>
</div>
<div className="flex flex-col p-6 md:p-10 bg-slate-50">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-12">
<div className="w-full max-w-[360px] bg-white p-8 rounded-2xl shadow-lg">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center mb-2">
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input
placeholder="아이디를 입력하세요"
className="h-11 rounded-xl bg-slate-50 border-0 focus:bg-white focus:ring-2 focus:ring-primary/20"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
placeholder="비밀번호를 입력하세요"
className="h-11 pr-10 rounded-xl bg-slate-50 border-0 focus:bg-white focus:ring-2 focus:ring-primary/20"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" className="rounded-md border-gray-300" />
<span className="text-gray-600"> </span>
</label>
<a href="#" className="text-primary hover:underline"> </a>
</div>
<Button className="w-full h-11 rounded-xl"></Button>
<Button variant="outline" className="w-full h-11 rounded-xl border-2">
</Button>
<div className="text-center">
<a href="#" className="text-sm text-gray-500 hover:text-primary"> </a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default function AuthPagesDemo() {
const designs = [
{
id: "current",
name: "현재",
description: "현재 적용된 디자인",
features: ["기존 레이아웃"],
component: LoginDesign1
},
{
id: "icon",
name: "시안 2",
description: "아이콘 + 비밀번호 토글 추가",
features: ["입력 필드 아이콘", "비밀번호 보기"],
component: LoginDesign2
},
{
id: "large",
name: "시안 3",
description: "더 큰 입력 필드 + 그림자",
features: ["h-12 입력필드", "그림자 효과", "부드러운 배경"],
component: LoginDesign3
},
{
id: "save",
name: "시안 4",
description: "아이디 저장 + 간결한 링크",
features: ["아이디 저장", "비밀번호 찾기 위치 변경"],
component: LoginDesign4
},
{
id: "color",
name: "시안 5",
description: "브랜드 컬러 강조",
features: ["컬러 배경", "컬러 아이콘", "컬러 제목"],
component: LoginDesign5
},
{
id: "round",
name: "시안 6",
description: "라운드 카드 스타일",
features: ["라운드 입력필드", "카드 레이아웃", "부드러운 그림자"],
component: LoginDesign6
}
];
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="text-gray-600 mt-2">
-
</p>
</div>
<Tabs defaultValue="current" className="space-y-6">
<TabsList className="grid grid-cols-6 w-full h-auto p-1">
{designs.map((design) => (
<TabsTrigger
key={design.id}
value={design.id}
className="py-3 text-xs sm:text-sm data-[state=active]:bg-primary data-[state=active]:text-white"
>
{design.name}
</TabsTrigger>
))}
</TabsList>
{designs.map((design) => (
<TabsContent key={design.id} value={design.id} className="space-y-4">
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h2 className="text-lg font-semibold">{design.name}: {design.description}</h2>
</div>
<div className="flex gap-2 flex-wrap">
{design.features.map((feature, idx) => (
<Badge key={idx} variant="secondary">{feature}</Badge>
))}
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<design.component />
</div>
</TabsContent>
))}
</Tabs>
</div>
</div>
);
}

View File

@@ -0,0 +1,455 @@
'use client';
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import Image from "next/image";
import { CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react";
// 시안 1: 현재 디자인 (3단계 스텝)
function SignupDesign1() {
const [step, setStep] = useState(1);
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground text-sm">
{step === 1 && "기본 정보"}
{step === 2 && "이메일 인증"}
{step === 3 && "추가 정보"}
</p>
</div>
{/* 스텝 인디케이터 */}
<div className="flex items-center justify-center gap-2 py-2">
{[1, 2, 3].map((s) => (
<div key={s} className="flex items-center">
<div className={cn(
"w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium",
step === s ? "bg-primary text-white" : step > s ? "bg-primary/20 text-primary" : "bg-gray-100 text-gray-400"
)}>
{step > s ? <CheckCircle2 className="w-4 h-4" /> : s}
</div>
{s < 3 && <div className={cn("w-8 h-0.5 mx-1", step > s ? "bg-primary/20" : "bg-gray-200")} />}
</div>
))}
</div>
{step === 1 && (
<>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Select><SelectTrigger><SelectValue placeholder="회원 유형을 선택하세요" /></SelectTrigger>
<SelectContent><SelectItem value="FARM"></SelectItem><SelectItem value="CNSLT"></SelectItem></SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="아이디를 입력하세요 (4자 이상)" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="이름을 입력하세요" />
</div>
</>
)}
{step === 2 && (
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input type="email" placeholder="이메일을 입력하세요" />
<Button variant="outline" className="w-full"> </Button>
</div>
)}
{step === 3 && (
<>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="010-0000-0000" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input type="password" placeholder="비밀번호 (8자 이상)" />
</div>
</>
)}
<div className="flex gap-2 pt-2">
{step > 1 && (
<Button variant="outline" onClick={() => setStep(s => s - 1)} className="flex-1">
<ChevronLeft className="w-4 h-4 mr-1" />
</Button>
)}
{step < 3 ? (
<Button onClick={() => setStep(s => s + 1)} className="flex-1"><ChevronRight className="w-4 h-4 ml-1" /></Button>
) : (
<Button className="flex-1"></Button>
)}
</div>
<Button variant="outline" className="w-full"> </Button>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 2: 스텝에 라벨 추가
function SignupDesign2() {
const [step, setStep] = useState(1);
const steps = [{ num: 1, label: "기본정보" }, { num: 2, label: "이메일인증" }, { num: 3, label: "추가정보" }];
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[360px]">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-bold"></h1>
</div>
{/* 스텝 인디케이터 with 라벨 */}
<div className="flex items-center justify-between py-4">
{steps.map((s, idx) => (
<div key={s.num} className="flex flex-col items-center flex-1">
<div className="flex items-center w-full">
{idx > 0 && <div className={cn("flex-1 h-0.5", step > idx ? "bg-primary" : "bg-gray-200")} />}
<div className={cn(
"w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium shrink-0",
step === s.num ? "bg-primary text-white" : step > s.num ? "bg-primary text-white" : "bg-gray-100 text-gray-400"
)}>
{step > s.num ? <CheckCircle2 className="w-5 h-5" /> : s.num}
</div>
{idx < 2 && <div className={cn("flex-1 h-0.5", step > s.num ? "bg-primary" : "bg-gray-200")} />}
</div>
<span className={cn("text-xs mt-2", step >= s.num ? "text-primary font-medium" : "text-gray-400")}>{s.label}</span>
</div>
))}
</div>
{step === 1 && (
<>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Select><SelectTrigger className="h-11"><SelectValue placeholder="회원 유형을 선택하세요" /></SelectTrigger>
<SelectContent><SelectItem value="FARM"></SelectItem><SelectItem value="CNSLT"></SelectItem></SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="아이디를 입력하세요 (4자 이상)" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="이름을 입력하세요" className="h-11" />
</div>
</>
)}
{step === 2 && (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input type="email" placeholder="이메일을 입력하세요" className="h-11" />
</div>
<Button variant="outline" className="w-full h-11"> </Button>
</div>
)}
{step === 3 && (
<>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="010-0000-0000" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input type="password" placeholder="비밀번호 (8자 이상)" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input type="password" placeholder="비밀번호를 다시 입력하세요" className="h-11" />
</div>
</>
)}
<div className="flex gap-2 pt-2">
{step > 1 && (
<Button variant="outline" onClick={() => setStep(s => s - 1)} className="flex-1 h-11">
<ChevronLeft className="w-4 h-4 mr-1" />
</Button>
)}
{step < 3 ? (
<Button onClick={() => setStep(s => s + 1)} className="flex-1 h-11"><ChevronRight className="w-4 h-4 ml-1" /></Button>
) : (
<Button className="flex-1 h-11"></Button>
)}
</div>
<Button variant="outline" className="w-full h-11"> </Button>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 3: 프로그레스 바 스타일
function SignupDesign3() {
const [step, setStep] = useState(1);
const progress = ((step - 1) / 2) * 100;
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground text-sm">
{step === 1 && "기본 정보를 입력해주세요"}
{step === 2 && "이메일 인증을 진행해주세요"}
{step === 3 && "마지막 단계입니다"}
</p>
</div>
{/* 프로그레스 바 */}
<div className="space-y-2">
<div className="flex justify-between text-xs text-gray-500">
<span> {step}/3</span>
<span>{Math.round(progress)}% </span>
</div>
<div className="w-full h-2 bg-gray-100 rounded-full overflow-hidden">
<div className="h-full bg-primary transition-all duration-300" style={{ width: `${progress}%` }} />
</div>
</div>
{step === 1 && (
<>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Select><SelectTrigger className="h-11"><SelectValue placeholder="회원 유형을 선택하세요" /></SelectTrigger>
<SelectContent><SelectItem value="FARM"></SelectItem><SelectItem value="CNSLT"></SelectItem></SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="아이디를 입력하세요" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="이름을 입력하세요" className="h-11" />
</div>
</>
)}
{step === 2 && (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input type="email" placeholder="이메일을 입력하세요" className="h-11" />
</div>
<Button variant="outline" className="w-full h-11"> </Button>
</div>
)}
{step === 3 && (
<>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="010-0000-0000" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input type="password" placeholder="비밀번호 (8자 이상)" className="h-11" />
</div>
</>
)}
<div className="flex gap-2 pt-2">
{step > 1 && (
<Button variant="outline" onClick={() => setStep(s => s - 1)} className="flex-1 h-11"></Button>
)}
{step < 3 ? (
<Button onClick={() => setStep(s => s + 1)} className="flex-1 h-11"></Button>
) : (
<Button className="flex-1 h-11"> </Button>
)}
</div>
<div className="text-center">
<a href="#" className="text-sm text-gray-500 hover:text-primary"> ? </a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// 시안 4: 현재 + 개선 (추천)
function SignupDesign4() {
const [step, setStep] = useState(1);
return (
<div className="grid min-h-[600px] lg:grid-cols-2 border rounded-lg overflow-hidden">
<div className="bg-white relative hidden lg:flex items-center justify-center">
<Image src="/logo-graphic.svg" alt="로고" fill className="object-contain p-16" priority />
</div>
<div className="flex flex-col p-6 md:p-10 bg-white">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground text-sm">
{step === 1 && "기본 정보"}
{step === 2 && "이메일 인증"}
{step === 3 && "추가 정보"}
</p>
</div>
{/* 스텝 인디케이터 */}
<div className="flex items-center justify-center gap-2 py-2">
{[1, 2, 3].map((s) => (
<div key={s} className="flex items-center">
<div className={cn(
"w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors",
step === s ? "bg-primary text-white" : step > s ? "bg-primary/20 text-primary" : "bg-gray-100 text-gray-400"
)}>
{step > s ? <CheckCircle2 className="w-4 h-4" /> : s}
</div>
{s < 3 && <div className={cn("w-8 h-0.5 mx-1 transition-colors", step > s ? "bg-primary/20" : "bg-gray-200")} />}
</div>
))}
</div>
{step === 1 && (
<>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Select><SelectTrigger className="h-11"><SelectValue placeholder="회원 유형을 선택하세요" /></SelectTrigger>
<SelectContent><SelectItem value="FARM"></SelectItem><SelectItem value="CNSLT"></SelectItem><SelectItem value="ORGAN"></SelectItem></SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="아이디를 입력하세요 (4자 이상)" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="이름을 입력하세요 (2자 이상)" className="h-11" />
</div>
</>
)}
{step === 2 && (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<div className="flex gap-2">
<Input type="text" placeholder="이메일 아이디" className="h-11 flex-1" />
<span className="flex items-center text-gray-400">@</span>
<Select><SelectTrigger className="h-11 flex-1"><SelectValue placeholder="선택" /></SelectTrigger>
<SelectContent><SelectItem value="gmail.com">gmail.com</SelectItem><SelectItem value="naver.com">naver.com</SelectItem></SelectContent>
</Select>
</div>
</div>
<Button variant="outline" className="w-full h-11"> </Button>
<p className="text-xs text-center text-green-600"> </p>
</div>
)}
{step === 3 && (
<>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input placeholder="010-0000-0000" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input type="password" placeholder="비밀번호를 입력하세요 (8자 이상)" className="h-11" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium"> *</label>
<Input type="password" placeholder="비밀번호를 다시 입력하세요" className="h-11" />
</div>
</>
)}
<div className="flex gap-2 pt-2">
{step > 1 && (
<Button variant="outline" onClick={() => setStep(s => s - 1)} className="flex-1 h-11">
<ChevronLeft className="w-4 h-4 mr-1" />
</Button>
)}
{step < 3 ? (
<Button onClick={() => setStep(s => s + 1)} className="flex-1 h-11"><ChevronRight className="w-4 h-4 ml-1" /></Button>
) : (
<Button className="flex-1 h-11"></Button>
)}
</div>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center"><span className="w-full border-t" /></div>
<div className="relative flex justify-center text-xs"><span className="bg-white px-2 text-gray-500"></span></div>
</div>
<Button variant="outline" className="w-full h-11 border-2 border-primary text-primary"></Button>
</div>
</div>
</div>
</div>
</div>
);
}
export default function SignupDemo() {
const designs = [
{ id: "current", name: "현재", description: "현재 적용된 3단계 스텝", features: ["숫자 인디케이터"], component: SignupDesign1 },
{ id: "label", name: "시안 2", description: "스텝에 라벨 추가", features: ["단계별 라벨", "연결선"], component: SignupDesign2 },
{ id: "progress", name: "시안 3", description: "프로그레스 바 스타일", features: ["진행률 바", "퍼센트 표시"], component: SignupDesign3 },
{ id: "improved", name: "시안 4", description: "현재 + 개선 (추천)", features: ["h-11 입력필드", "로그인 통일 스타일"], component: SignupDesign4 },
];
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="text-gray-600 mt-2"> </p>
</div>
<Tabs defaultValue="current" className="space-y-6">
<TabsList className="grid grid-cols-4 w-full h-auto p-1">
{designs.map((design) => (
<TabsTrigger key={design.id} value={design.id} className="py-3 text-xs sm:text-sm data-[state=active]:bg-primary data-[state=active]:text-white">
{design.name}
</TabsTrigger>
))}
</TabsList>
{designs.map((design) => (
<TabsContent key={design.id} value={design.id} className="space-y-4">
<div className="bg-white p-4 rounded-lg border">
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h2 className="text-lg font-semibold">{design.name}: {design.description}</h2>
</div>
<div className="flex gap-2 flex-wrap">
{design.features.map((feature, idx) => (
<Badge key={idx} variant="secondary">{feature}</Badge>
))}
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<design.component />
</div>
</TabsContent>
))}
</Tabs>
</div>
</div>
);
}

View File

@@ -0,0 +1,331 @@
'use client';
import { useState } from "react";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ChevronDown } from "lucide-react";
const emailDomains = [
'gmail.com',
'naver.com',
'daum.net',
'hanmail.net',
'nate.com',
'kakao.com',
'직접입력',
];
// 시안 1: 직접입력 시 별도 행에 입력창
function EmailDomain1() {
const [emailId, setEmailId] = useState('');
const [emailDomain, setEmailDomain] = useState('');
const [customDomain, setCustomDomain] = useState('');
return (
<div className="space-y-4">
<h3 className="font-semibold"> 1: 직접입력 </h3>
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<Input
placeholder="이메일"
value={emailId}
onChange={(e) => setEmailId(e.target.value)}
className="flex-1 h-11"
/>
<span className="text-muted-foreground">@</span>
<Select value={emailDomain} onValueChange={setEmailDomain}>
<SelectTrigger className="flex-1 h-11">
<SelectValue placeholder="도메인 선택" />
</SelectTrigger>
<SelectContent>
{emailDomains.map((domain) => (
<SelectItem key={domain} value={domain}>
{domain}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{emailDomain === '직접입력' && (
<Input
placeholder="도메인을 입력하세요 (예: company.com)"
value={customDomain}
onChange={(e) => setCustomDomain(e.target.value)}
className="h-11"
/>
)}
</div>
<p className="text-sm text-muted-foreground">
: {emailId}@{emailDomain === '직접입력' ? customDomain : emailDomain}
</p>
</div>
);
}
// 시안 2: 직접입력 시 드롭다운 자리에 인풋 + 옆에 드롭다운 버튼
function EmailDomain2() {
const [emailId, setEmailId] = useState('');
const [emailDomain, setEmailDomain] = useState('');
const [customDomain, setCustomDomain] = useState('');
const [isSelectOpen, setIsSelectOpen] = useState(false);
return (
<div className="space-y-4">
<h3 className="font-semibold"> 2: 인풋 + </h3>
<div className="flex gap-2 items-center">
<Input
placeholder="이메일"
value={emailId}
onChange={(e) => setEmailId(e.target.value)}
className="flex-1 h-11"
/>
<span className="text-muted-foreground">@</span>
{emailDomain === '직접입력' ? (
<div className="flex flex-1 gap-1">
<Input
placeholder="도메인 입력"
value={customDomain}
onChange={(e) => setCustomDomain(e.target.value)}
className="flex-1 h-11"
/>
<Select value={emailDomain} onValueChange={(v) => {
setEmailDomain(v);
if (v !== '직접입력') setCustomDomain('');
}}>
<SelectTrigger className="w-11 h-11 px-0 justify-center">
<ChevronDown className="h-4 w-4" />
</SelectTrigger>
<SelectContent>
{emailDomains.map((domain) => (
<SelectItem key={domain} value={domain}>
{domain}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : (
<Select value={emailDomain} onValueChange={setEmailDomain}>
<SelectTrigger className="flex-1 h-11">
<SelectValue placeholder="도메인 선택" />
</SelectTrigger>
<SelectContent>
{emailDomains.map((domain) => (
<SelectItem key={domain} value={domain}>
{domain}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<p className="text-sm text-muted-foreground">
: {emailId}@{emailDomain === '직접입력' ? customDomain : emailDomain}
</p>
</div>
);
}
// 시안 3: Combobox 스타일 - 인풋이면서 드롭다운
function EmailDomain3() {
const [emailId, setEmailId] = useState('');
const [emailDomain, setEmailDomain] = useState('');
const [customDomain, setCustomDomain] = useState('');
const [isOpen, setIsOpen] = useState(false);
const displayValue = emailDomain === '직접입력' ? customDomain : emailDomain;
return (
<div className="space-y-4">
<h3 className="font-semibold"> 3: Combobox ( + )</h3>
<div className="flex gap-2 items-center">
<Input
placeholder="이메일"
value={emailId}
onChange={(e) => setEmailId(e.target.value)}
className="flex-1 h-11"
/>
<span className="text-muted-foreground">@</span>
<div className="relative flex-1">
<Input
placeholder="도메인 선택 또는 입력"
value={displayValue}
onChange={(e) => {
setEmailDomain('직접입력');
setCustomDomain(e.target.value);
}}
onFocus={() => setIsOpen(true)}
className="h-11 pr-10"
/>
<Select
value={emailDomain}
onValueChange={(v) => {
setEmailDomain(v);
if (v !== '직접입력') setCustomDomain('');
setIsOpen(false);
}}
open={isOpen}
onOpenChange={setIsOpen}
>
<SelectTrigger className="absolute right-0 top-0 w-10 h-11 border-0 bg-transparent hover:bg-transparent focus:ring-0">
<ChevronDown className="h-4 w-4" />
</SelectTrigger>
<SelectContent>
{emailDomains.map((domain) => (
<SelectItem key={domain} value={domain}>
{domain}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<p className="text-sm text-muted-foreground">
: {emailId}@{displayValue}
</p>
</div>
);
}
// 시안 4: 드롭다운 영역과 입력 영역 분리
function EmailDomain4() {
const [emailId, setEmailId] = useState('');
const [emailDomain, setEmailDomain] = useState('');
const [customDomain, setCustomDomain] = useState('');
return (
<div className="space-y-4">
<h3 className="font-semibold"> 4: 드롭다운 + ( )</h3>
<div className="flex gap-2 items-center">
<Input
placeholder="이메일"
value={emailId}
onChange={(e) => setEmailId(e.target.value)}
className="w-[140px] h-11"
/>
<span className="text-muted-foreground shrink-0">@</span>
<div className="flex-1 flex gap-1">
{emailDomain === '직접입력' ? (
<Input
placeholder="도메인 입력"
value={customDomain}
onChange={(e) => setCustomDomain(e.target.value)}
className="flex-1 h-11"
/>
) : (
<div className="flex-1" />
)}
<Select
value={emailDomain}
onValueChange={(v) => {
setEmailDomain(v);
if (v !== '직접입력') setCustomDomain('');
}}
>
<SelectTrigger className={emailDomain === '직접입력' ? "w-[100px] h-11" : "w-full h-11"}>
<SelectValue placeholder="도메인 선택" />
</SelectTrigger>
<SelectContent>
{emailDomains.map((domain) => (
<SelectItem key={domain} value={domain}>
{domain}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<p className="text-sm text-muted-foreground">
: {emailId}@{emailDomain === '직접입력' ? customDomain : emailDomain}
</p>
</div>
);
}
// 시안 5: 인풋과 드롭다운 자연스럽게 통합 (하나의 필드처럼 보이게)
function EmailDomain5() {
const [emailId, setEmailId] = useState('');
const [emailDomain, setEmailDomain] = useState('');
const [customDomain, setCustomDomain] = useState('');
return (
<div className="space-y-4">
<h3 className="font-semibold"> 5: 인풋 + </h3>
<div className="flex gap-2 items-center">
<Input
placeholder="이메일"
value={emailId}
onChange={(e) => setEmailId(e.target.value)}
className="flex-1 h-11"
/>
<span className="text-muted-foreground shrink-0">@</span>
<div className="flex items-center flex-1 h-11 border rounded-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
{emailDomain === '직접입력' ? (
<Input
placeholder="도메인 입력"
value={customDomain}
onChange={(e) => setCustomDomain(e.target.value)}
className="flex-1 h-full border-0 focus-visible:ring-0 focus-visible:ring-offset-0 rounded-r-none"
/>
) : (
<span className="flex-1 px-3 text-sm truncate">
{emailDomain || <span className="text-muted-foreground"> </span>}
</span>
)}
<Select
value={emailDomain}
onValueChange={(v) => {
setEmailDomain(v);
if (v !== '직접입력') setCustomDomain('');
}}
>
<SelectTrigger className="w-10 h-full border-0 bg-transparent px-0 focus:ring-0 rounded-l-none justify-center" />
<SelectContent>
{emailDomains.map((domain) => (
<SelectItem key={domain} value={domain}>
{domain}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<p className="text-sm text-muted-foreground">
: {emailId}@{emailDomain === '직접입력' ? customDomain : emailDomain}
</p>
</div>
);
}
export default function EmailDomainDemo() {
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-2xl mx-auto space-y-8">
<div>
<h1 className="text-2xl font-bold"> UI </h1>
<p className="text-muted-foreground mt-2">
</p>
</div>
<div className="bg-white p-6 rounded-lg border space-y-8">
<EmailDomain1 />
<hr />
<EmailDomain2 />
<hr />
<EmailDomain3 />
<hr />
<EmailDomain4 />
<hr />
<EmailDomain5 />
</div>
</div>
</div>
);
}

View File

@@ -37,7 +37,7 @@ export default function FindIdPage() {
/>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<FindIdForm />
</div>

View File

@@ -31,7 +31,7 @@ export default function FindPwPage() {
/>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<FindPwForm />
</div>

View File

@@ -2,24 +2,43 @@
import { LoginForm } from "@/components/auth/login-form";
import { useAuthStore } from '@/store/auth-store';
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useState, useEffect } from "react";
import Image from "next/image";
const SAVED_USER_ID_KEY = 'savedUserId';
// 로그인 페이지 컴포넌트
export default function LoginPage() {
const [userId, setUserId] = useState('')
const [userPassword, setUserPassword] = useState('')
const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [saveId, setSaveId] = useState(false)
const router = useRouter()
const { login } = useAuthStore();
const { login } = useAuthStore();
// 로그인 처리 함수
// 저장된 아이디 불러오기
useEffect(() => {
const savedUserId = localStorage.getItem(SAVED_USER_ID_KEY)
if (savedUserId) {
setUserId(savedUserId)
setSaveId(true)
}
}, [])
// 로그인 처리 함수
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setIsLoading(true)
// 아이디 저장 처리
if (saveId) {
localStorage.setItem(SAVED_USER_ID_KEY, userId)
} else {
localStorage.removeItem(SAVED_USER_ID_KEY)
}
try {
await login({
userId: userId,
@@ -72,7 +91,7 @@ export default function LoginPage() {
/>
</div>
<div className="flex flex-1 items-center justify-start lg:pl-24">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<LoginForm
onSubmit={handleLogin}
@@ -82,6 +101,8 @@ export default function LoginPage() {
setUserPassword={setUserPassword}
error={error}
isLoading={isLoading}
saveId={saveId}
setSaveId={setSaveId}
/>
</div>
</div>

View File

@@ -30,6 +30,7 @@ export default function SignupPage() {
const [isLoading, setIsLoading] = useState(false)
const [emailCheckStatus, setEmailCheckStatus] = useState<'idle' | 'checking' | 'available' | 'unavailable'>('idle')
const [emailCheckMessage, setEmailCheckMessage] = useState('')
const [currentStep, setCurrentStep] = useState(1)
const router = useRouter()
const { signup } = useAuthStore()
@@ -233,7 +234,7 @@ export default function SignupPage() {
/>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="flex flex-1 items-center justify-center lg:justify-start lg:pl-16">
<div className="w-full max-w-[320px] lg:max-w-sm">
<SignupForm
onSubmit={handleSignup}
@@ -251,6 +252,8 @@ export default function SignupPage() {
isVerifyingCode={isVerifyingCode}
emailCheckStatus={emailCheckStatus}
emailCheckMessage={emailCheckMessage}
currentStep={currentStep}
setCurrentStep={setCurrentStep}
/>
</div>
</div>