Skip to content

Commit cb8f4e9

Browse files
ksh277claude
andcommitted
Fix TopBanner layout and CategoryShortcuts implementation
- TopBanner: Remove margins for full-width display, fix main_title and sub_title rendering - CategoryShortcuts: Implement CENTER alignment with 55px gaps, 160px circles, font-based titles - Remove hardcoded temporary category data, make admin-only management - Hide CategoryShortcuts on mobile devices - Fix TypeScript errors in banners API - Remove obsolete components and files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 85c0e24 commit cb8f4e9

24 files changed

+874
-439
lines changed

migrate-banners.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
const mysql = require('mysql2/promise');
2+
require('dotenv').config({ path: '.env.local' });
3+
4+
async function migrateBannersTable() {
5+
let connection;
6+
7+
try {
8+
// 데이터베이스 연결
9+
connection = await mysql.createConnection(process.env.DATABASE_URL);
10+
console.log('✅ Google Cloud SQL MySQL 연결 성공');
11+
12+
// 현재 banners 테이블 구조 확인
13+
console.log('\n📋 현재 banners 테이블 구조:');
14+
const [columns] = await connection.execute('DESCRIBE banners');
15+
console.table(columns);
16+
17+
// 필요한 컬럼들이 이미 존재하는지 확인
18+
const existingColumns = columns.map(col => col.Field);
19+
const columnsToAdd = [
20+
{ name: 'banner_type', exists: existingColumns.includes('banner_type') },
21+
{ name: 'main_title', exists: existingColumns.includes('main_title') },
22+
{ name: 'sub_title', exists: existingColumns.includes('sub_title') },
23+
{ name: 'more_button_link', exists: existingColumns.includes('more_button_link') },
24+
{ name: 'device_type', exists: existingColumns.includes('device_type') }
25+
];
26+
27+
console.log('\n🔍 컬럼 존재 여부 확인:');
28+
columnsToAdd.forEach(col => {
29+
console.log(` ${col.name}: ${col.exists ? '✅ 존재' : '❌ 없음'}`);
30+
});
31+
32+
// 필요한 컬럼들 추가
33+
const alterStatements = [];
34+
35+
if (!columnsToAdd.find(c => c.name === 'banner_type').exists) {
36+
alterStatements.push("ADD COLUMN banner_type VARCHAR(50) DEFAULT 'IMAGE_BANNER' AFTER href");
37+
}
38+
39+
if (!columnsToAdd.find(c => c.name === 'main_title').exists) {
40+
alterStatements.push("ADD COLUMN main_title VARCHAR(255) NULL AFTER banner_type");
41+
}
42+
43+
if (!columnsToAdd.find(c => c.name === 'sub_title').exists) {
44+
alterStatements.push("ADD COLUMN sub_title VARCHAR(255) NULL AFTER main_title");
45+
}
46+
47+
if (!columnsToAdd.find(c => c.name === 'more_button_link').exists) {
48+
alterStatements.push("ADD COLUMN more_button_link VARCHAR(255) NULL AFTER sub_title");
49+
}
50+
51+
if (!columnsToAdd.find(c => c.name === 'device_type').exists) {
52+
alterStatements.push("ADD COLUMN device_type VARCHAR(20) DEFAULT 'all' AFTER more_button_link");
53+
}
54+
55+
if (alterStatements.length === 0) {
56+
console.log('\n✅ 모든 필요한 컬럼이 이미 존재합니다!');
57+
return;
58+
}
59+
60+
// ALTER TABLE 실행
61+
console.log('\n🔄 데이터베이스 마이그레이션 시작...');
62+
const alterQuery = `ALTER TABLE banners ${alterStatements.join(', ')}`;
63+
console.log('실행할 쿼리:', alterQuery);
64+
65+
await connection.execute(alterQuery);
66+
console.log('✅ 마이그레이션 완료!');
67+
68+
// 업데이트된 테이블 구조 확인
69+
console.log('\n📋 업데이트된 banners 테이블 구조:');
70+
const [updatedColumns] = await connection.execute('DESCRIBE banners');
71+
console.table(updatedColumns);
72+
73+
// 인덱스 추가 (성능 향상)
74+
console.log('\n🔄 인덱스 추가 중...');
75+
try {
76+
await connection.execute('CREATE INDEX idx_banner_type ON banners(banner_type)');
77+
console.log('✅ banner_type 인덱스 추가 완료');
78+
} catch (e) {
79+
if (e.code !== 'ER_DUP_KEYNAME') {
80+
console.log('⚠️ banner_type 인덱스 추가 실패:', e.message);
81+
} else {
82+
console.log('ℹ️ banner_type 인덱스 이미 존재');
83+
}
84+
}
85+
86+
try {
87+
await connection.execute('CREATE INDEX idx_device_type ON banners(device_type)');
88+
console.log('✅ device_type 인덱스 추가 완료');
89+
} catch (e) {
90+
if (e.code !== 'ER_DUP_KEYNAME') {
91+
console.log('⚠️ device_type 인덱스 추가 실패:', e.message);
92+
} else {
93+
console.log('ℹ️ device_type 인덱스 이미 존재');
94+
}
95+
}
96+
97+
console.log('\n🎉 전체 마이그레이션이 성공적으로 완료되었습니다!');
98+
99+
} catch (error) {
100+
console.error('❌ 마이그레이션 실패:', error);
101+
throw error;
102+
} finally {
103+
if (connection) {
104+
await connection.end();
105+
console.log('🔐 데이터베이스 연결 종료');
106+
}
107+
}
108+
}
109+
110+
// 스크립트 실행
111+
if (require.main === module) {
112+
migrateBannersTable()
113+
.then(() => {
114+
console.log('\n✅ 마이그레이션 스크립트 완료');
115+
process.exit(0);
116+
})
117+
.catch((error) => {
118+
console.error('\n❌ 마이그레이션 스크립트 실패:', error);
119+
process.exit(1);
120+
});
121+
}
122+
123+
module.exports = { migrateBannersTable };

prisma/schema.prisma

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,26 @@ model User {
4848
}
4949

5050
model banners {
51-
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
52-
title String @db.VarChar(255)
53-
image_url String @db.VarChar(512)
54-
href String? @db.VarChar(255)
55-
start_at DateTime @db.DateTime(0)
56-
end_at DateTime @db.DateTime(0)
57-
sort_order Int? @default(0)
58-
is_active Boolean? @default(true)
59-
created_at DateTime? @default(now()) @db.Timestamp(0)
60-
updated_at DateTime? @default(now()) @updatedAt @db.Timestamp(0)
51+
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
52+
title String @db.VarChar(255)
53+
image_url String @db.VarChar(512)
54+
href String? @db.VarChar(255)
55+
banner_type String? @default("IMAGE_BANNER") @db.VarChar(50)
56+
main_title String? @db.VarChar(255)
57+
sub_title String? @db.VarChar(255)
58+
more_button_link String? @db.VarChar(255)
59+
device_type String? @default("all") @db.VarChar(20)
60+
start_at DateTime @db.DateTime(0)
61+
end_at DateTime @db.DateTime(0)
62+
sort_order Int? @default(0)
63+
is_active Boolean? @default(true)
64+
created_at DateTime? @default(now()) @db.Timestamp(0)
65+
updated_at DateTime? @default(now()) @updatedAt @db.Timestamp(0)
6166
6267
@@index([is_active, start_at, end_at], map: "idx_active_dates")
6368
@@index([sort_order], map: "idx_sort_order")
69+
@@index([banner_type], map: "idx_banner_type")
70+
@@index([device_type], map: "idx_device_type")
6471
}
6572

6673
model addresses {

src/app/api/banners/[id]/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export async function PUT(req: Request, { params }: Params) {
4141
if (data.title !== undefined) updateData.title = data.title;
4242
if (data.image_url !== undefined) updateData.image_url = data.image_url;
4343
if (data.href !== undefined) updateData.href = data.href;
44+
if (data.main_title !== undefined) updateData.main_title = data.main_title;
45+
if (data.sub_title !== undefined) updateData.sub_title = data.sub_title;
46+
if (data.more_button_link !== undefined) updateData.more_button_link = data.more_button_link;
4447
if (data.banner_type !== undefined) updateData.banner_type = data.banner_type;
4548
if (data.device_type !== undefined) updateData.device_type = data.device_type;
4649
if (data.is_active !== undefined) updateData.is_active = Boolean(data.is_active);

src/app/api/banners/route.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export async function GET(req: Request) {
1111
const bannerType = searchParams.get('banner_type');
1212
const deviceType = searchParams.get('device_type');
1313

14-
const whereClause: any = includeInactive ? {} : { is_active: true };
14+
let whereClause: any = includeInactive ? {} : { is_active: true };
1515

1616
if (bannerType) {
1717
whereClause.banner_type = bannerType;
@@ -25,8 +25,8 @@ export async function GET(req: Request) {
2525
}
2626

2727
const orderByClause = sortBy === 'sort_order'
28-
? { sort_order: sortOrder, created_at: 'desc' }
29-
: { [sortBy]: sortOrder };
28+
? { sort_order: sortOrder as 'asc' | 'desc', created_at: 'desc' as const }
29+
: { [sortBy]: sortOrder as 'asc' | 'desc' };
3030

3131
const items = await prisma.banners.findMany({
3232
where: whereClause,
@@ -40,6 +40,9 @@ export async function GET(req: Request) {
4040
imgSrc: item.image_url,
4141
alt: item.title,
4242
title: item.title,
43+
mainTitle: item.main_title,
44+
subTitle: item.sub_title,
45+
moreButtonLink: item.more_button_link,
4346
bannerType: item.banner_type,
4447
deviceType: item.device_type,
4548
isActive: item.is_active,
@@ -73,16 +76,16 @@ export async function POST(req: Request) {
7376
const sortOrder = data.sort_order !== undefined ? parseInt(data.sort_order) : 0;
7477
const isActive = data.is_active !== undefined ? Boolean(data.is_active) : true;
7578

76-
// 배너 타입별 제한 확인 (여기서는 간단히 처리, 실제로는 더 정교한 로직 필요)
79+
// 배너 타입별 제한 확인
7780
const existingCount = await prisma.banners.count({
7881
where: {
7982
banner_type: bannerType,
8083
is_active: true
81-
}
84+
} as any
8285
});
8386

8487
const limits: { [key: string]: number } = {
85-
'TOP_BANNER': 1,
88+
'TOP_BANNER': 8,
8689
'STRIP_BANNER': 1,
8790
'HOME_SLIDER_PC': 2,
8891
'HOME_SLIDER_MOBILE': 1,
@@ -96,18 +99,23 @@ export async function POST(req: Request) {
9699
);
97100
}
98101

102+
const createData: any = {
103+
title: data.title.trim(),
104+
image_url: data.image_url.trim(),
105+
href: data.href?.trim() || null,
106+
main_title: data.main_title?.trim() || null,
107+
sub_title: data.sub_title?.trim() || null,
108+
more_button_link: data.more_button_link?.trim() || null,
109+
banner_type: bannerType,
110+
device_type: deviceType,
111+
is_active: isActive,
112+
sort_order: sortOrder,
113+
start_at: data.start_at ? new Date(data.start_at) : new Date(),
114+
end_at: data.end_at ? new Date(data.end_at) : new Date('2025-12-31'),
115+
};
116+
99117
const created = await prisma.banners.create({
100-
data: {
101-
title: data.title.trim(),
102-
image_url: data.image_url.trim(),
103-
href: data.href?.trim() || null,
104-
banner_type: bannerType,
105-
device_type: deviceType,
106-
is_active: isActive,
107-
sort_order: sortOrder,
108-
start_at: data.start_at ? new Date(data.start_at) : new Date(),
109-
end_at: data.end_at ? new Date(data.end_at) : new Date('2025-12-31'),
110-
},
118+
data: createData,
111119
});
112120

113121
const result = {

src/app/api/category-shortcuts/route.js

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,18 @@ export async function GET() {
1414

1515
const transformedShortcuts = shortcuts.map(shortcut => ({
1616
id: shortcut.id.toString(),
17+
title: shortcut.title,
18+
image_url: shortcut.image_url,
1719
href: shortcut.href,
18-
label: shortcut.title,
19-
imgSrc: shortcut.image_url,
20-
sortOrder: shortcut.sort_order
20+
sort_order: shortcut.sort_order,
21+
is_active: shortcut.is_active
2122
}));
2223

2324
return NextResponse.json(transformedShortcuts);
2425

2526
} catch (error) {
2627
console.error('Category shortcuts API error:', error);
27-
28-
// 데이터베이스 오류 시 기본 데이터 반환
29-
const fallbackShortcuts = [
30-
{ id: '1', href: '/category/1만원이하굿즈', label: '1만원 이하 굿즈', imgSrc: 'https://placehold.co/100x100/FFB6C1/333?text=💰' },
31-
{ id: '2', href: '/category/야구굿즈', label: '야구 굿즈', imgSrc: 'https://placehold.co/100x100/87CEEB/333?text=⚾' },
32-
{ id: '3', href: '/category/여행굿즈', label: '여행 굿즈', imgSrc: 'https://placehold.co/100x100/98FB98/333?text=✈️' },
33-
{ id: '4', href: '/category/팬굿즈', label: '팬 굿즈', imgSrc: 'https://placehold.co/100x100/DDA0DD/333?text=💜' },
34-
{ id: '5', href: '/category/폰꾸미기', label: '폰꾸미기', imgSrc: 'https://placehold.co/100x100/FFE4B5/333?text=📱' },
35-
{ id: '6', href: '/category/반려동물굿즈', label: '반려동물 굿즈', imgSrc: 'https://placehold.co/100x100/F0E68C/333?text=🐾' },
36-
{ id: '7', href: '/category/선물추천', label: '선물 추천', imgSrc: 'https://placehold.co/100x100/F5DEB3/333?text=🎁' },
37-
{ id: '8', href: '/category/커스텀아이디어', label: '커스텀 아이디어', imgSrc: 'https://placehold.co/100x100/E6E6FA/333?text=💡' }
38-
];
39-
40-
return NextResponse.json(fallbackShortcuts);
28+
return NextResponse.json([]);
4129
}
4230
}
4331

@@ -90,10 +78,11 @@ export async function POST(request) {
9078

9179
return NextResponse.json({
9280
id: created.id.toString(),
81+
title: created.title,
82+
image_url: created.image_url,
9383
href: created.href,
94-
label: created.title,
95-
imgSrc: created.image_url,
96-
sortOrder: created.sort_order
84+
sort_order: created.sort_order,
85+
is_active: created.is_active
9786
}, { status: 201 });
9887

9988
} catch (error) {
@@ -146,10 +135,11 @@ export async function PUT(request) {
146135

147136
return NextResponse.json({
148137
id: updated.id.toString(),
138+
title: updated.title,
139+
image_url: updated.image_url,
149140
href: updated.href,
150-
label: updated.title,
151-
imgSrc: updated.image_url,
152-
sortOrder: updated.sort_order
141+
sort_order: updated.sort_order,
142+
is_active: updated.is_active
153143
});
154144

155145
} catch (error) {

src/app/api/create-category-shortcuts-table/route.ts

Lines changed: 0 additions & 74 deletions
This file was deleted.

0 commit comments

Comments
 (0)