Skip to content
Merged
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
7 changes: 5 additions & 2 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { UsersModule } from '../users/users.module';
Expand All @@ -7,7 +7,10 @@ import { AuthService } from '../auth/service/auth.service';
import { Token, TokenSchema } from '../auth/schemas/token.schema';

@Module({
imports: [MongooseModule.forFeature([{ name: Token.name, schema: TokenSchema }]), UsersModule],
imports: [
MongooseModule.forFeature([{ name: Token.name, schema: TokenSchema }]),
forwardRef(() => UsersModule),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
Expand Down
4 changes: 2 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express'; // [1] 타입 추가
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import cookieParser from 'cookie-parser';

async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.set('trust proxy', 1); // 프록스 설정
app.set('trust proxy', 1);
app.use(cookieParser());

app.useGlobalPipes(
Expand Down
2 changes: 1 addition & 1 deletion src/menus/seed/menu.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MenuContext } from '../enum/menu-context.enum';
export const menuSeedData = [
// 한식
{
name: '김치찌개',
name: '국밥',
category: MenuCategory.KOREAN,

contexts: [MenuContext.LUNCH, MenuContext.SOLO, MenuContext.LIGHT],
Expand Down
11 changes: 11 additions & 0 deletions src/users/dto/add-food-dot.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString, Length, Matches } from 'class-validator';

export class AddFoodDotDto {
@IsString()
@IsNotEmpty({ message: 'dotId는 필수 값입니다.' })
@Length(1, 50, { message: 'dotId는 1~50자여야 합니다.' })
@Matches(/^[a-zA-Z0-9_-]+$/, {
message: 'dotId는 영문, 숫자, -, _ 만 허용됩니다.',
})
dotId: string;
}
5 changes: 4 additions & 1 deletion src/users/schemas/user.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class User {
@Prop({ required: true, unique: true, lowercase: true, trim: true })
email!: string;

@Prop({ type: String, required: false, default: null }) // 타입 명시
@Prop({ type: String, required: false, default: null })
passwordHash?: string | null;

@Prop({ required: true, trim: true })
Expand All @@ -17,6 +17,9 @@ export class User {
@Prop({ type: String, default: null })
profileImage!: string | null;

@Prop({ type: [String], default: [] })
selectedFoodDotIds!: string[];

createdAt!: Date;
updatedAt!: Date;
}
Expand Down
68 changes: 68 additions & 0 deletions src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
Controller,
Get,
Post,
Delete,
Param,
Body,
Req,
UnauthorizedException,
Header,
} from '@nestjs/common';
import type { Request } from 'express';

import { AuthService } from '../auth/service/auth.service';
import { UsersService } from './users.service';
import { AddFoodDotDto } from './dto/add-food-dot.dto';

interface AuthCookies {
accessToken?: string;
refreshToken?: string;
}

interface AuthRequest extends Request {
cookies: AuthCookies;
}

@Controller('users')
export class UsersController {
constructor(
private readonly usersService: UsersService,
private readonly authService: AuthService,
) {}

private async extractUserId(req: AuthRequest): Promise<string> {
const accessToken = req.cookies?.accessToken;

if (!accessToken) {
throw new UnauthorizedException('인증 토큰이 없습니다');
}

const user = await this.authService.verifyAccessToken(accessToken);
return user._id.toString();
}

// 내 음식 도트 목록 조회
@Get('me/food-dots')
@Header('Cache-Control', 'no-store, no-cache, must-revalidate, private')
@Header('Pragma', 'no-cache')
@Header('Expires', '0')
async getMyFoodDots(@Req() req: AuthRequest) {
const userId = await this.extractUserId(req);
return this.usersService.getMyFoodDots(userId);
}

// 음식 도트 추가
@Post('me/food-dots')
async addFoodDot(@Req() req: AuthRequest, @Body() body: AddFoodDotDto) {
const userId = await this.extractUserId(req);
return this.usersService.addFoodDot(userId, body.dotId);
}

// 음식 도트 제거
@Delete('me/food-dots/:dotId')
async removeFoodDot(@Req() req: AuthRequest, @Param('dotId') dotId: string) {
const userId = await this.extractUserId(req);
return this.usersService.removeFoodDot(userId, dotId);
}
}
10 changes: 8 additions & 2 deletions src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { User, UserSchema } from './schemas/user.schema';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { AuthModule } from '../auth/auth.module';

@Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
forwardRef(() => AuthModule),
],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
Expand Down
70 changes: 66 additions & 4 deletions src/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';

Expand All @@ -10,6 +10,8 @@ export type UserUpdateData = {
profileImage?: string | null;
};

const MAX_FOOD_DOTS = 9;

@Injectable()
export class UsersService {
constructor(
Expand All @@ -24,15 +26,15 @@ export class UsersService {
}

// ID로 유저 조회
async findById(userId: Types.ObjectId | string) {
findById(userId: Types.ObjectId | string) {
return this.userModel.findById(userId).exec();
}

// 회원 생성
async create(userData: {
nickname: string;
email: string;
passwordHash?: string; // oauth 사용자도 고려
passwordHash?: string;
profileImage?: string | null;
}) {
const user = new this.userModel(userData);
Expand All @@ -44,7 +46,7 @@ export class UsersService {
const updatedUser = await this.userModel.findByIdAndUpdate(
userId,
{ $set: updateData },
{ new: true }, // 업데이트된 문서 반환
{ new: true },
);

if (!updatedUser) {
Expand All @@ -53,4 +55,64 @@ export class UsersService {

return updatedUser;
}

// 내 음식 도트 목록 조회
async getMyFoodDots(userId: Types.ObjectId | string): Promise<string[]> {
const user = await this.userModel.findById(userId).select('selectedFoodDotIds');

if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다');
}

return user.selectedFoodDotIds ?? [];
}

// 음식 도트 추가
async addFoodDot(userId: Types.ObjectId | string, dotId: string): Promise<string[]> {
const updatedUser = await this.userModel.findOneAndUpdate(
{
_id: userId,
$expr: {
$lt: [{ $size: { $ifNull: ['$selectedFoodDotIds', []] } }, MAX_FOOD_DOTS],
},
},
{
$addToSet: { selectedFoodDotIds: dotId },
},
{
new: true,
},
);

if (!updatedUser) {
const exists = await this.userModel.exists({ _id: userId });

if (!exists) {
throw new NotFoundException('사용자를 찾을 수 없습니다');
}

throw new BadRequestException(`음식 도트는 최대 ${MAX_FOOD_DOTS}개까지 선택할 수 있습니다`);
}

return updatedUser.selectedFoodDotIds ?? [];
}

// 음식 도트 제거
async removeFoodDot(userId: Types.ObjectId | string, dotId: string): Promise<string[]> {
const updatedUser = await this.userModel.findByIdAndUpdate(
userId,
{
$pull: { selectedFoodDotIds: dotId },
},
{
new: true,
},
);

if (!updatedUser) {
throw new NotFoundException('사용자를 찾을 수 없습니다');
}

return updatedUser.selectedFoodDotIds ?? [];
}
}