Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"dependencies": {
"@apollo/server": "^5.2.0",
"@as-integrations/express5": "^1.1.2",
"@aws-sdk/client-s3": "^3.1027.0",
"@aws-sdk/s3-request-presigner": "^3.1027.0",
"@nestjs/apollo": "13.2.3",
"@nestjs/common": "11.1.10",
"@nestjs/config": "^4.0.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
Warnings:

- You are about to drop the `review_image` table. If the table is not empty, all the data it contains will be lost.

*/
-- DropForeignKey
ALTER TABLE `review_image` DROP FOREIGN KEY `review_image_review_id_fkey`;

-- DropTable
DROP TABLE `review_image`;

-- CreateTable
CREATE TABLE `review_media` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`review_id` BIGINT UNSIGNED NOT NULL,
`media_type` ENUM('IMAGE', 'VIDEO') NOT NULL DEFAULT 'IMAGE',
`media_url` VARCHAR(2048) NOT NULL,
`thumbnail_url` VARCHAR(2048) NULL,
`sort_order` INTEGER NOT NULL DEFAULT 0,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
`deleted_at` DATETIME(3) NULL,

INDEX `idx_review_media_review`(`review_id`, `sort_order`),
INDEX `idx_review_media_deleted_at`(`deleted_at`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `recent_product_view` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`account_id` BIGINT UNSIGNED NOT NULL,
`product_id` BIGINT UNSIGNED NOT NULL,
`viewed_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
`deleted_at` DATETIME(3) NULL,

INDEX `idx_recent_product_view_account_viewed`(`account_id`, `viewed_at`),
INDEX `idx_recent_product_view_deleted_at`(`deleted_at`),
UNIQUE INDEX `uk_recent_product_view`(`account_id`, `product_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `review_media` ADD CONSTRAINT `review_media_review_id_fkey` FOREIGN KEY (`review_id`) REFERENCES `review`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `recent_product_view` ADD CONSTRAINT `recent_product_view_account_id_fkey` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `recent_product_view` ADD CONSTRAINT `recent_product_view_product_id_fkey` FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
51 changes: 43 additions & 8 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ enum ConversationBodyFormat {
HTML
}

enum ReviewMediaType {
IMAGE
VIDEO
}

enum SearchContext {
GLOBAL
NEIGHBORHOOD
Expand Down Expand Up @@ -154,6 +159,7 @@ model Account {
store_conversations StoreConversation[]
sent_conversation_messages StoreConversationMessage[] @relation("ConversationMessageSender")
store Store? @relation("StoreSellerAccount")
recent_product_views RecentProductView[]

@@index([account_type, status], map: "idx_account_type_status")
@@index([deleted_at], map: "idx_account_deleted_at")
Expand Down Expand Up @@ -452,7 +458,8 @@ model Product {
reviews Review[]
banners Banner[] @relation("BannerProductLink")

notifications Notification[]
notifications Notification[]
recent_product_views RecentProductView[]

@@index([store_id], map: "idx_product_store")
@@index([name], map: "idx_product_name")
Expand Down Expand Up @@ -1009,7 +1016,7 @@ model Review {
store Store @relation(fields: [store_id], references: [id])
product Product @relation(fields: [product_id], references: [id])

images ReviewImage[]
media ReviewMedia[]

likes ReviewLike[]
notifications Notification[]
Expand All @@ -1021,22 +1028,24 @@ model Review {
@@map("review")
}

model ReviewImage {
model ReviewMedia {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
review_id BigInt @db.UnsignedBigInt

image_url String @db.VarChar(2048)
sort_order Int @default(0)
media_type ReviewMediaType @default(IMAGE)
media_url String @db.VarChar(2048)
thumbnail_url String? @db.VarChar(2048)
sort_order Int @default(0)

created_at DateTime @default(now()) @db.DateTime(3)
updated_at DateTime @updatedAt @db.DateTime(3)
deleted_at DateTime? @db.DateTime(3)

review Review @relation(fields: [review_id], references: [id])

@@index([review_id, sort_order], map: "idx_review_image_review")
@@index([deleted_at], map: "idx_review_image_deleted_at")
@@map("review_image")
@@index([review_id, sort_order], map: "idx_review_media_review")
@@index([deleted_at], map: "idx_review_media_deleted_at")
@@map("review_media")
}

/**
Expand Down Expand Up @@ -1348,3 +1357,29 @@ model AuditLog {
@@index([target_type, target_id, created_at], map: "idx_audit_target_time")
@@map("audit_log")
}

/**
* =========================
* 17) Recent Product View (최근 본 상품)
* =========================
*/

model RecentProductView {
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
account_id BigInt @db.UnsignedBigInt
product_id BigInt @db.UnsignedBigInt

viewed_at DateTime @default(now()) @db.DateTime(3)

created_at DateTime @default(now()) @db.DateTime(3)
updated_at DateTime @updatedAt @db.DateTime(3)
deleted_at DateTime? @db.DateTime(3)

account Account @relation(fields: [account_id], references: [id])
product Product @relation(fields: [product_id], references: [id])

@@unique([account_id, product_id], map: "uk_recent_product_view")
@@index([account_id, viewed_at], map: "idx_recent_product_view_account_viewed")
@@index([deleted_at], map: "idx_recent_product_view_deleted_at")
@@map("recent_product_view")
}
3 changes: 2 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import authConfig from '@/config/auth.config';
import databaseConfig from '@/config/database.config';
import docsConfig from '@/config/docs.config';
import oidcConfig from '@/config/oidc.config';
import s3Config from '@/config/s3.config';
import { AuthModule } from '@/features/auth/auth.module';
import { SellerModule } from '@/features/seller/seller.module';
import { SystemModule } from '@/features/system/system.module';
Expand All @@ -33,7 +34,7 @@ import { PrismaModule } from '@/prisma';
ConfigModule.forRoot({
isGlobal: true,
cache: true,
load: [authConfig, databaseConfig, docsConfig, oidcConfig],
load: [authConfig, databaseConfig, docsConfig, oidcConfig, s3Config],
}),
ServeStaticModule.forRoot({
rootPath: join(process.cwd(), 'public'),
Expand Down
40 changes: 40 additions & 0 deletions src/config/s3.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { registerAs } from '@nestjs/config';

/**
* S3 설정 타입
*/
export interface S3Config {
region: string;
bucket: string;
accessKeyId?: string;
secretAccessKey?: string;
presignExpiresSeconds: number;
}

/**
* S3 설정
*
* 운영 환경에서는 IAM Role 사용을 권장하며,
* 로컬/개발 환경에서는 Access Key로 인증한다.
*/
export default registerAs('s3', (): S3Config => {
const isProd = process.env.NODE_ENV === 'production';
const bucket = process.env.AWS_S3_BUCKET?.trim() ?? '';

if (isProd && !bucket) {
throw new Error('AWS_S3_BUCKET must be set in production environment');
}

const presignExpires = Number(process.env.S3_PRESIGN_EXPIRES_SECONDS);

return {
region: process.env.AWS_REGION?.trim() || 'ap-northeast-2',
bucket: bucket || 'caquick-media-dev',
accessKeyId: process.env.AWS_ACCESS_KEY_ID?.trim() || undefined,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY?.trim() || undefined,
presignExpiresSeconds:
Number.isFinite(presignExpires) && presignExpires > 0
? presignExpires
: 600,
};
});
3 changes: 2 additions & 1 deletion src/prisma/soft-delete.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const SOFT_DELETE_MODELS = new Set<Prisma.ModelName>([
'OrderItemCustomFreeEdit',
'OrderItemCustomFreeEditAttachment',
'Review',
'ReviewImage',
'ReviewMedia',
'Notification',
'SearchHistory',
'SearchEvent',
Expand All @@ -47,6 +47,7 @@ const SOFT_DELETE_MODELS = new Set<Prisma.ModelName>([
'StoreConversationMessage',
'StoreFaqTopic',
'StoreDailyCapacity',
'RecentProductView',
]);

const READ_ACTIONS = new Set<Prisma.PrismaAction>([
Expand Down
Loading
Loading