PROJECT๐Ÿ’ป

[Web] ์ž์ทจ๋งŒ๋ ™ ํ”„๋กœ์ ํŠธ

์ด๋ฆฌ์ญ 2025. 2. 11. 04:20

ํ•ด๋‹น ํ”„๋กœ์ ํŠธ GitHub Repository

https://github.com/2024-Hansung-Capstone/ProjectJ-Backend

 

GitHub - 2024-Hansung-Capstone/ProjectJ-Backend

Contribute to 2024-Hansung-Capstone/ProjectJ-Backend development by creating an account on GitHub.

github.com

 

1. ์ฃผ์ œ ์„ค๋ช…

 ํ•œ์„ฑ๋Œ€ํ•™๊ต ์กธ์—… ์ž‘ํ’ˆ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.

์กธ์—… ์ž‘ํ’ˆ์ธ ๋งŒํผ ์ฃผ์ œ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ๊ฝค ๊ธธ์—ˆ๊ณ , ์ฃผ์ œ๋ฅผ ์ œ ์ฃผ๋ณ€์—์„œ ์ฐพ๋Š” ๊ฒƒ์ด ๊ดœ์ฐฎ์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‹ค๊ฐ€ ์‚ฌ์ดŒ๋™์ƒ์ด ์ž์ทจ๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด์„œ ์ž์ทจ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ๊ฝค ๋งŽ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ , ์กฐ๊ธˆ์ด๋‚˜๋งˆ ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ ํ•ด์„œ ์ž์ทจ์ƒ์„ ์œ„ํ•œ ์›น์‚ฌ์ดํŠธ๋กœ ์ฃผ์ œ๋ฅผ ์„ ์ •ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

 ์šฐ์„ , ์ž์ทจํ•  ๋•Œ ์‚ฌ์šฉ์ž๋“ค์ด ์–ด๋–ค ์Œ์‹์„ ํ•ด ๋จน์„์ง€ ๊ณ ๋ฏผ์„ ๋งŽ์ด ํ•  ๊ฒƒ ๊ฐ™์•„์„œ AI ๊ธฐ๋Šฅ์ด ํƒ‘์žฌ๋œ ์š”๋ฆฌ ์ถ”์ฒœ ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ์ž์ทจ๋ฅผ ์ด์ œ ๋ง‰ ์‹œ์ž‘ํ•˜๋Š” ์‚ฌ์šฉ์ž๋“ค์„ ์œ„ํ•ด์„œ ์›๋ฃธ ์œ„์น˜์™€ ์ •๋ณด๋ฅผ ์ง€๋„์—์„œ ํ•œ๋ˆˆ์— ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ , ์ž์ทจํ•˜๋ฉด์„œ ํ•„์š”ํ•œ ๋ฌผํ’ˆ์„ ์‚ฌ์šฉ์ž๋“ค๋ผ๋ฆฌ ๊ฑฐ๋ž˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ค‘๊ณ ๋งˆ์ผ“ ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ, ์ž์ทจ์ƒ๋“ค์—๊ฒŒ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์ด ๋ชจ๋‘ ๊ฐ–์ถฐ์ง„ ์›น์‚ฌ์ดํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜์—ฌ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.


2. ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„

2024.01.25 ~ 2024.05.30 (์•ฝ 4๊ฐœ์›”)


3. ์—ญํ• 

๋ฐฑ์—”๋“œ ์ด๊ด„, ๋ฐฐํฌ ์„œ๋ฒ„ ๊ด€๋ฆฌ
  • ์„ค๊ณ„
    • ERD ์„ค๊ณ„
    • ๋กœ๊ทธ์ธ ๊ณผ์ • ์„ค๊ณ„
    • ํ–‰์ •๊ธฐ๊ด€ ์ฝ”๋“œ ์„ค๊ณ„
    • ์‚ฌ์šฉ์ž ๋“ฑ๊ธ‰ ๊ตฌ์„ฑ
  • ๊ตฌํ˜„
    • ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„
    • ์š”๋ฆฌ CRUD ๊ตฌํ˜„, ์š”๋ฆฌ AI ๊ตฌํ˜„
    • ์ชฝ์ง€ ๊ธฐ๋Šฅ ๊ตฌํ˜„
    • ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„
    • ํฌ์ธํŠธ ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • ๋ฐฐํฌ
    • AWS EC2 ์ธ์Šคํ„ด์Šค ๋ฐฐํฌ
    • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ S3 ์‚ฌ์šฉ

4. ๊ธฐ์ˆ  ์Šคํƒ

  • ์–ธ์–ด: TypeScript
  • ํ”„๋ ˆ์ž„์›Œํฌ: NestJS
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: MySQL
  • ๋ฐฐํฌ: AWS EC2, S3, Elastic IP
  • ๋„๊ตฌ: Docker, GraphQL, OpenAI API, CoolSMS

5. ERD


6. ์ „์ฒด ๊ตฌ์กฐ


7. ์ƒ์„ธ ๊ตฌํ˜„ ๋‚ด์šฉ

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์˜ ํ•ต์‹ฌ ๊ธฐ์ˆ  ์Šคํƒ์€ NestJS์™€ GraphQL๋กœ ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

GraphQL์„ ์‚ฌ์šฉํ•œ ์ด์œ 

ํ•™๊ต์—์„œ๋Š” RestAPI ๋ฐฉ์‹๋งŒ์„ ๋ฐฐ์› ๋Š”๋ฐ ์ €ํฌ ํŒ€์—๊ฒŒ ์ƒ์†Œํ•œ GraphQL์„ ๋„์ž…ํ•˜๊ฒŒ ๋œ ์ด์œ ๋Š” ๋‘ ๊ฐ€์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
์ฒซ ๋ฒˆ์งธ๋กœ, ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋งŒ์„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ด์ ์ด ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
๋‘ ๋ฒˆ์งธ๋กœ, ํ•™๊ต์—์„œ ์ •ํ•ด์ค€ ๊ธฐ๊ฐ„์ด ์ด‰๋ฐ•ํ•ด์„œ Swagger์™€ ๊ฐ™์€ ๋ฌธ์„œํ™” ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ธฐ์— ์‹œ๊ฐ„์ด ๋ถ€์กฑํ•  ์ˆ˜๋„ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐ์„ ํ•˜์—ฌ ๋‹ค๋ฅธ ๋ฐฉ์‹์„ ์ƒ๊ฐ์„ ํ•ด ๋ณด๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋Ÿฌ๋‹ค๊ฐ€ GraphQL ๋ฐฉ์‹์€ Playground๋ฅผ ํ†ตํ•ด Api๊ฐ€ ์ž๋™์œผ๋กœ ๋ฌธ์„œํ™”๋˜๊ณ , ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ์‚ฌ์šฉํ•ด ๋ณผ ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์ด ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณต๋˜๋Š” ์ด์ ์ด ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜๋ฉด์„œ ๋„์ž…ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

7-1. ํšŒ์› ๊ด€๋ฆฌ

  • JWT๋ฅผ ์ด์šฉํ•œ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ

์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ์™€ ๋กœ๊ทธ์ธ ์œ ์ง€๋ฅผ ์œ„ํ•ด์„œ JWT ๊ธฐ๋Šฅ์„ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค.

JWT ํ† ํฐ์€ accessToken๊ณผ restoreToken์œผ๋กœ ๊ตฌ์„ฑํ•˜์˜€๊ณ , accessToken์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ 10๋ถ„, restoreToken์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ 2์ฃผ ์ •๋„๋กœ ์žก์•˜์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, accessToken์ด ๋งŒ๋ฃŒ๋˜์–ด๋„ restoreToken์„ ํ†ตํ•ด accessToken ์žฌ๋ฐœ๊ธ‰์ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์„œ ์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์œ ์ง€ ์‹œ๊ฐ„์„ ์•ˆ์ •์ ์œผ๋กœ ์ฆ๊ฐ€์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. 

 

[accessToken ๋ฐœ๊ธ‰]

/**
 * JWT ํ† ํฐ ์ƒ์„ฑ ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ
 * @param email ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ
 * @param password ์‚ฌ์šฉ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ
 * @returns accessToken
 */
getAccessToken(user: User | IUserContext['user']): string {
  return this.jwtService.sign(
    { sub: user.id },
    { secret: process.env.JWT_ACCESS_SECRET, expiresIn: '100m' },
  );
}

 

[restoreToken์„ ํ†ตํ•œ accessToken ์žฌ๋ฐœ๊ธ‰]

/**
 * JWT ํ† ํฐ ๋งŒ๋ฃŒ ๋Œ€๋น„ refresh ํ† ํฐ ์ƒ์„ฑ ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ
 * @param user ์‚ฌ์šฉ์ž ์ •๋ณด
 * @param context context
 */
getRefreshToken(user: User, context: IContext): void {
  const refreshToken = this.jwtService.sign(
    { sub: user.id },
    { secret: process.env.JWT_REFRESH_SECRET, expiresIn: '2w' },
  );

  context.res.setHeader(
    'set-cookie',
    `refreshToken=${refreshToken}; path=/;`,
  );
}

 

  • ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ Resolver ๊ธฐ๋Šฅ ๋ณดํ˜ธ

ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ์™€ ๊ฐ™์€ ๊ธฐ๋Šฅ์€ ๋กœ๊ทธ์ธ ํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋„ ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด์ง€๋งŒ ํšŒ์› ์ •๋ณด ์ˆ˜์ •, ๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •/์‚ญ์ œ ๋“ฑ์˜ ๊ธฐ๋Šฅ์€ ์ธ์ฆ์ด ๋œ ์‚ฌ์šฉ์ž๋งŒ ํ˜ธ์ถœ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ •ํ•ด์ค˜์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, ๋ณดํ˜ธ๊ฐ€ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์—๊ฒŒ `@UseGuards` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋ถ™ํ˜€์คฌ๊ณ , `AuthGuard('jwt')`๋ฅผ ์ธ์ž๋กœ ๋„ฃ์–ด์„œ ํ•ด๊ฒฐํ•˜๋ ค ํ–ˆ์ง€๋งŒ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

Cannot read properties of undefined (reading 'headers') ์˜ค๋ฅ˜๊ฐ€ ๋ฐœํ–‰ํ•œ ์ด์œ 

์ž˜ ์ž‘๋™๋˜์ง€ ์•Š์€ ์ด์œ ๋Š” GraphQL ํ™˜๊ฒฝ์—์„œ request ๊ฐ์ฒด๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณต๋˜์ง€ ์•Š์•„์„œ request ๋‚ด์— ์žˆ๋Š” header ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
๋”ฐ๋ผ์„œ, GraphQL์—์„œ UseGuards๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ header์˜ ๊ฐ’์„ ์ •์ƒ์ ์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์œ„ํ•ด AuthGuard๋ฅผ extendํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์ฃผ์–ด ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.

 

[GraphQL ํ™˜๊ฒฝ์„ ์œ„ํ•ด ์ƒ์„ฑํ•œ AuthGuard]

export class gqlAuthAccessGuard extends AuthGuard('heoga') {
  getRequest(context: ExecutionContext) {
    const gqlContext = GqlExecutionContext.create(context);
    return gqlContext.getContext().req;
  }
}

 

๊ทธ๋ฆฌ๊ณ , 'heoga'๋ผ๊ณ  ์ด๋ฆ„์„ ์ง€์€ PassportStrategy๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

export default class JwtAccessStrategy extends PassportStrategy(Strategy, 'heoga') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_ACCESS_SECRET,
    });
  }

  validate(payload) {
    console.log(payload);
    return {
      id: payload.sub,
    };
  }
}

 

์ด์ œ @UseGuard()์˜ ์ธ์ž๋กœ ์œ„์—์„œ ๋งŒ๋“  GqlAuthAccessGuard๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด ํ•ด๋‹น ๊ธฐ๋Šฅ์€ GraphQL ํ™˜๊ฒฝ์—์„œ๋„ JWT Access Token ์ธ์ฆ์ด ๋œ ์ƒํƒœ์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

@UseGuards(gqlAccessGuard)
@Mutation(() => User, {
  description: 'ํ˜„์žฌ ๋กœ๊ทธ์ธ ๋œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.',
})
async updateUser(
  ...
}

 

7-2. Open AI ๋„์ž…

์š”๋ฆฌ ๋ ˆ์‹œํ”ผ ์ž๋™ ์ƒ์„ฑ์„ ์œ„ํ•ด์„œ๋Š” AI ๊ธฐ๋Šฅ ๋„์ž…์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, ์ €ํฌ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” Open AI API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ ˆ์‹œํ”ผ๋ฅผ ์ž๋™ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์ €ํฌ๊ฐ€ AI๋ฅผ ํ™œ์šฉํ•ด์„œ ๋งŒ๋“ค์–ด์•ผ ํ–ˆ๋˜ ๊ธฐ๋Šฅ์˜ ํ๋ฆ„์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž์—๊ฒŒ ์‹์žฌ๋ฃŒ ์ž…๋ ฅ๋ฐ›๊ธฐ -> ์‹์žฌ๋ฃŒ ์ •๋ณด Open AI์—๊ฒŒ ์ „๋‹ฌ -> Open AI๊ฐ€ ํ•ด๋‹น ์‹์žฌ๋ฃŒ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ์š”๋ฆฌ ์„ ์ • -> ํ•ด๋‹น ์š”๋ฆฌ์˜ ํ•„์š” ์žฌ๋ฃŒ์™€ ์กฐ๋ฆฌ๋ฐฉ๋ฒ• ๋“ฑ์„ JSON์œผ๋กœ ์ƒ์„ฑ -> ํ”„๋ก ํŠธ์—”๋“œ์—๊ฒŒ GraphQL Query๋กœ ๋„˜๊ฒจ์คŒ

 

ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด Open AI์—์„œ ์ง€์›ํ•˜๋Š” ๊ธฐ๋Šฅ ์ค‘ assistant ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

Open AI Assistant

Open AI๊ฐ€ ์ง€์›ํ•˜๋Š” ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๋กœ, ํ•ด๋‹น assistant์—๊ฒŒ ๋ช…๋ นํ•  ๋ช…๋ น์–ด๋ฅผ ์›น์—์„œ ๋ฏธ๋ฆฌ ์ง€์ •ํ•ด๋†“์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๊ทธ ์ดํ›„์— ํ•ด๋‹น assistant์˜ id๋ฅผ ํ†ตํ•ด API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ฏธ๋ฆฌ ์ง€์ •ํ•ด๋†จ๋˜ ๋ช…๋ น์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒฐ๊ณผ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งค๋ฒˆ ๋™์ผํ•œ ๋ช…๋ น์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๊ณ , assistant์—์„œ ์ง€์›ํ•˜๋Š” JSON ๋ฐฉ์‹์˜ return์ด ์œ ์šฉํ•œ ์ƒํ™ฉ์ด์—ˆ๋˜ ์ €ํฌ ํ”„๋กœ์ ํŠธ์— ๋„์ž…ํ•˜๊ธฐ ์•Œ๋งž์€ ๊ธฐ๋Šฅ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

 

[์‹์žฌ๋ฃŒ ์ •๋ณด๋ฅผ AI์—๊ฒŒ ์ „๋‹ฌํ•˜์—ฌ ๋ ˆ์‹œํ”ผ ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ค๋Š” Service]

async getRecipes(user_id: string) {
  const myIngredients = await this.findIngredientByUserId(user_id);
  const ingredientsInfo = myIngredients.map((ingredient) => ({
    name: ingredient.name,
    volume: ingredient.volume,
    volume_unit: ingredient.volume_unit,
  }));

  const headers = {
    Authorization: `Bearer ${process.env.OPENAI_SECRET}`,
    'Content-Type': 'application/json',
    'OpenAI-Beta': 'assistants=v2',
  };

  const run = await this.httpService
    .post(
      `https://api.openai.com/v1/threads/runs`,
      {
        assistant_id: 'asst_YFQJAwlpvKdljoQuYnZZHBDt',
        thread: {
          messages: [
            {
              role: 'user',
              content: JSON.stringify(ingredientsInfo),
            },
          ],
        },
      },
      { headers: headers },
    )
    .pipe(map((response) => response.data))
    .toPromise();

  let result;
  do {
    await new Promise((resolve) => setTimeout(resolve, 5000));
    result = await this.httpService
      .get(
        `https://api.openai.com/v1/threads/${run.thread_id}/runs/${run.id}`,
        {
          headers: headers,
        },
      )
      .toPromise();
  } while (result.data.status !== 'completed');

  const response = await this.httpService
    .get(`https://api.openai.com/v1/threads/${run.thread_id}/messages`, {
      headers: headers,
    })
    .toPromise();

  return JSON.parse(response.data.data[0].content[0].text.value).recipes;
}

 

7-3. ์ง€์—ญ ์ฝ”๋“œ ๊ด€๋ฆฌ

์‚ฌ์šฉ์ž ์ง€์—ญ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ํ–‰์ •์•ˆ์ „๋ถ€์—์„œ 2024๋…„ 2์›” 1์ผ ๊ธฐ์ค€์˜ ํ–‰์ •๋™ ์ฝ”๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

https://www.mois.go.kr/frt/bbs/type001/commonSelectBoardArticle.do?bbsId=BBSMSTR_000000000052&nttId=106692

 

ํ–‰์ •๊ธฐ๊ด€(ํ–‰์ •๋™) ๋ฐ ๊ด€ํ• ๊ตฌ์—ญ(๋ฒ•์ •๋™) ๋ณ€๊ฒฝ๋‚ด์—ญ(2024.2.1. ์‹œํ–‰) | ํ–‰์ •์•ˆ์ „๋ถ€> ์—…๋ฌด์•ˆ๋‚ด> ์ฐจ๊ด€๋ณด>

ํ–‰์ •์•ˆ์ „๋ถ€ ํ™ˆํŽ˜์ด์ง€์— ์˜ค์‹ ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค.

www.mois.go.kr

 

์‹œ/๋„, ์‹œ/๊ตฐ/๊ตฌ, ์/๋ฉด/๋™ ๋‹จ์œ„๋กœ ์ฝ”๋“œ๊ฐ€ ๋‚˜๋‰˜์–ด์ง„ ๊ฒƒ์„ ํŒŒ์•…ํ–ˆ๊ณ , ์•„๋ž˜์™€ ๊ฐ™์€ ๊ตฌ์กฐ๋กœ ์ฝ”๋“œ๊ฐ€ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ex) ํ˜œํ™”๋™

1 1 / 1 1 0 / 6 5 0 0 0
---   -----   ---------
์‹œ๋„  ์‹œ๊ตฐ๊ตฌ    ํ–‰์ •๋™

 

๊ฒฐ๊ณผ์ ์œผ๋กœ, ๊ฐ๊ฐ ํ…Œ์ด๋ธ”๋กœ ๋งŒ๋“ค์–ด ๊ฐ ํ•˜์œ„ ๋‹จ๊ณ„์˜ ์ง€์—ญ ๊ธฐ์ค€ ํ…Œ์ด๋ธ”๊ณผ One To Many ๊ด€๊ณ„๋ฅผ ์ด๋ฃจ์–ด ์ง€์—ญ์ฝ”๋“œ๋ฅผ ๊ด€๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

 

7-4. ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„

์•Œ๋ฆผ์ด ์ƒ์„ฑ๋˜๋Š” ๋‹ค์–‘ํ•œ ์ƒํ™ฉ๋“ค์ด ์žˆ์–ด ์šฐ์„  ํ•ด๋‹น ์ƒํ™ฉ๋“ค์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. ์‹ ๊ทœ ๊ฐ€์ž… ์‹œ
  2. ๋‚ด ์ œํ’ˆ์— ์ฐœ์ด ์ƒ๊ธธ ์‹œ
  3. ๋‚ด๊ฐ€ ์ฐœํ•œ ์ œํ’ˆ์˜ ๊ฐ€๊ฒฉ์ด ๋ณ€๊ฒฝ๋  ์‹œ
  4. ๋‚ด ๊ฒŒ์‹œ๊ธ€์— ์ข‹์•„์š”๊ฐ€ ์ƒ๊ธธ ์‹œ
  5. ๋‚ด ๋Œ“๊ธ€์— ์ข‹์•„์š”๊ฐ€ ์ƒ๊ธธ ์‹œ
  6. ๋‚ด ๊ฒŒ์‹œ๊ธ€์— ๋Œ“๊ธ€์ด ๋‹ฌ๋ฆด ์‹œ
  7. ์ชฝ์ง€๊ฐ€ ์˜ฌ ์‹œ

์ด ์™ธ์—๋„ ์ดํ›„ ๋‹ค์–‘ํ•œ ์•Œ๋ฆผ์ด ์ถ”๊ฐ€๋  ์ˆ˜ ์žˆ์–ด์„œ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๊ฐ€ ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ, HTTP์˜ Status Code ๊ด€๋ฆฌ ๋ฐฉ์‹๊ณผ ๋น„์Šทํ•˜๊ฒŒ ์•Œ๋ฆผ ์ƒํ™ฉ๋“ค์„ ๊ด€๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

  • 1XX : ์‚ฌ์šฉ์ž ๊ด€๋ จ
  • 2XX : ์ข‹์•„์š”/์ฐœ ๊ด€๋ จ
  • 3XX : ๊ฒŒ์‹œ๊ธ€ ๊ด€๋ จ
  • 4XX : ์ชฝ์ง€ ๊ด€๋ จ

๊ฒฐ๊ณผ์ ์œผ๋กœ, ์•Œ๋ฆผ ์ƒํ™ฉ ๋ณ„ ๋ฉ”์‹œ์ง€๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์—ˆ๊ณ , ์—ฌ๊ธฐ์—์„œ ์ƒํ™ฉ ๋ณ„๋กœ ์ฝ”๋“œ๋ฅผ ๋ถ€์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, ์ฝ”๋“œ ๋ณ„๋กœ ํ•ด๋‹น ์ฝ”๋“œ์˜ ์ƒํ™ฉ์— ๋งž๋Š” message๋ฅผ ๊ฐ–๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

[์•Œ๋ฆผ ์ƒํ™ฉ ๋ณ„ message ๊ด€๋ฆฌ ํด๋ž˜์Šค]

export class NotificationMessages {

  /**
   * ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ๋ฉ”์„œ๋“œ
   * ์•Œ๋ฆผ ์ฝ”๋“œ์— ๋”ฐ๋ผ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
   * @param notification ๋ฉ”์‹œ์ง€๋ฅผ ์กฐํšŒํ•  ์•Œ๋ฆผ ์ •๋ณด
   * @returns ์ƒ์„ฑ๋œ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€
   */
  async getMessage(notification: Notification): Promise<string> {
    let message = '';
    try {
      switch (notification.code) {
        case '100':
          message = `${notification.user.name}๋‹˜์˜ ์‹ ๊ทœ ํšŒ์›๊ฐ€์ž…์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!`;
          break;
        case '200':
          message = `${notification.like.user.name}๋‹˜์ด '${notification.used_product.title}' ์ œํ’ˆ์„ ์ฐœํ•˜์˜€์Šต๋‹ˆ๋‹ค.`;
          break;
        case '201':
          message = `๋‚ด๊ฐ€ ์ฐœํ•œ '${notification.used_product.title}'์˜ ๊ฐ€๊ฒฉ์ด ${notification.used_product.price}์›์œผ๋กœ ๋ณ€๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`;
          break;
        case '202':
          message = `๊ฒŒ์‹œ๊ธ€ '${notification.board.title}'์— ${notification.like.user.name}๋‹˜์ด ์ข‹์•„์š”๋ฅผ ๋ˆŒ๋ €์Šต๋‹ˆ๋‹ค.`;
          break;
        case '203':
          message = `'${notification.board.title}' ๊ฒŒ์‹œ๊ธ€์— ๋‹จ ๋‚ด ๋Œ“๊ธ€์— ${notification.like.user.name}๋‹˜์ด ์ข‹์•„์š”๋ฅผ ๋ˆŒ๋ €์Šต๋‹ˆ๋‹ค.`;
          break;
        case '300':
          message = `๊ฒŒ์‹œ๊ธ€ '${notification.board.title}'์— ${notification.reply.user.name}๋‹˜์ด ๋Œ“๊ธ€์„ ๋‚จ๊ฒผ์Šต๋‹ˆ๋‹ค.`;
          break;
        case '400':
          message = `'${notification.letter.sender.name}๋‹˜์ด ${notification.letter.category} ์นดํ…Œ๊ณ ๋ฆฌ์—์„œ ์ชฝ์ง€๋ฅผ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค.`;
          break;
        default:
          message = '์•Œ ์ˆ˜ ์—†๋Š” ์•Œ๋ฆผ์ž…๋‹ˆ๋‹ค.';
          break;
      }
      return message;
    } catch (e) {
      console.error(e);
      throw e;
    }
  }
}

 

๊ทธ๋ฆฌ๊ณ , ์•Œ๋ฆผ ์ •๋ณด๊ฐ€ ์žˆ๋Š” ์—”ํ‹ฐํ‹ฐ์—์„œ ํ•ด๋‹น ์•Œ๋ฆผ์ด ์‚ฌ์šฉ์ž ๊ด€๋ จ ์ผ ์ˆ˜๋„ ์žˆ๊ณ , ๊ฒŒ์‹œ๊ธ€์ด๋‚˜ ๋Œ“๊ธ€, ์ข‹์•„์š” ํ˜น์€ ์ชฝ์ง€ ๊ด€๋ จ ์•Œ๋ฆผ ์ผ ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋“  ์ •๋ณด๊ฐ€ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ•˜๊ณ  ํ•ด๋‹น ์•Œ๋ฆผ๊ณผ ๊ด€๋ จ ์—†๋Š” ํ•„๋“œ๋Š” NULL์„ ๋„ฃ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ , Strategy ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•˜์—ฌ ์•Œ๋ฆผ ๋ณ„๋กœ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ ๋งค์นญํ•˜๊ณ  ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์„ฑํ•˜์—ฌ ์•Œ๋ฆผ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

[์ชฝ์ง€ ๊ด€๋ จ์ธ 400๋ฒˆ๋Œ€ ์•Œ๋ฆผ์ผ ๋•Œ ์ ์šฉํ•  strategy]

export class LetterNotificationStrategy implements NotificationStrategy {
  constructor(
    private letterService: LetterService,
    private notificationRepository: Repository<Notification>,
  ) {}

  async createNotification(
    entity_id: string,
    code: string,
  ): Promise<Notification> {
    const letter = await this.letterService.findById(entity_id);
    return await this.notificationRepository.save({
      user: letter.receiver,
      code: code,
      letter: letter,
    });
  }
}

 

์ตœ์ข…์ ์œผ๋กœ ํ•ด๋‹น ์•Œ๋ฆผ์„ ์ƒ์„ฑ์‹œํ‚ฌ ์ƒํ™ฉ์˜ service์—์„œ ์•Œ๋ฆผ์„ ์ƒ์„ฑํ•˜๋Š” service๋ฅผ ๋ถˆ๋Ÿฌ์™€ ์•Œ๋ฆผ ๊ตฌํ˜„์„ ์„ฑ๊ณตํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

7-5. ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ ํŒจํ‚ค์ง•

๋กœ์ปฌ์ด ์•„๋‹Œ aws์˜ EC2 ์ธ์Šคํ„ด์Šค ํ™˜๊ฒฝ์—์„œ ์ˆ˜์›”ํ•œ ๋ฐฐํฌ๋ฅผ ์œ„ํ•˜์—ฌ Docker๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

Docker์˜ ์‚ฌ์šฉ์œผ๋กœ ๋‹ค๋ฅธ ์šด์˜์ฒด์ œ ํ™˜๊ฒฝ์—์„œ๋„ ๋™์ผํ•œ ํ”„๋กœ์ ํŠธ ์‹คํ–‰ ํ™˜๊ฒฝ์„ ๊ฐ–์ถ”์–ด ์•ˆ์ •์ ์ธ ์‹คํ–‰์ด ๊ฐ€๋Šฅํ–ˆ์Šต๋‹ˆ๋‹ค.

 

[Dockerfile]

# 1. ์šด์˜์ฒด์ œ ์„ค์น˜(node ์ตœ์‹ ๋ฒ„์ „๊ณผ npm๊ณผ yarn์ด ๋ชจ๋‘ ์„ค์น˜๋˜์–ด์žˆ๋Š” ๋ฆฌ๋ˆ…์Šค)
FROM node:latest

# 2. ๋‚ด ์ปดํ“จํ„ฐ์— ์žˆ๋Š” ํด๋”๋‚˜ ํŒŒ์ผ์„ ๋„์ปค ์ปดํ“จํ„ฐ ์•ˆ์œผ๋กœ ๋ณต์‚ฌํ•˜๊ธฐ
COPY ./package.json /ProjectJ-Backend/
COPY ./yarn.lock /ProjectJ-Backend/
WORKDIR /ProjectJ-Backend/
RUN yarn install
ENV NODE_OPTIONS="--max-old-space-size=1024"
COPY . /ProjectJ-Backend/

# 3. ๋„์ปค์•ˆ์—์„œ index.js ์‹คํ–‰์‹œํ‚ค๊ธฐ
CMD yarn start:dev
ENV NODE_OPTIONS="--max-old-space-size=1024"๋ฅผ ๋„ฃ์€ ์ด์œ 

AWS์—์„œ Free Tier๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ €์žฅ ๊ณต๊ฐ„๊ณผ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ํ•œ์ •์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค.
Swap ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ํ†ตํ•ด ์–ด๋Š ์ •๋„ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์˜ฌ๋ ธ์ง€๋งŒ, JavaScript heap out of memory ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ์„œ ์ตœ๋Œ€ ํž™ ๋ฉ”๋ชจ๋ฆฌ ์ œํ•œ์„ ์œ„ํ•ด ํ•ด๋‹น ์˜ต์…˜์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ธ MySQL ํ™˜๊ฒฝ๋„ ํ•„์š”ํ•˜์˜€๋Š”๋ฐ, EC2 ์ธ์Šคํ„ด์Šค์—์„œ MySQL์„ ์„ค์น˜ํ•˜๋Š” ๋Œ€์‹ ์— Docker์˜ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ„ํŽธํ•  ๊ฒƒ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, ๋‘ ํ™˜๊ฒฝ์„ ํ•˜๋‚˜๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด docker-compose๋ฅผ ๋„์ž…ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

[docker-compose.yaml]

version: '3.7'

services:
  backend:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./src:/ProjectJ-Backend/src
    ports:
      - '${SERVER_EXTERNAL_PORT}:${SERVER_INTERNAL_PORT}'
    env_file:
      - ./.env.docker

  database:
    image: mysql:latest
    environment:
      MYSQL_DATABASE: ${DATABASE_DATABASE}
      MYSQL_ROOT_PASSWORD: ${DATABASE_PASSWORD}
    ports:
      - '${DATABASE_EXTERNAL_PORT}:${DATABASE_INTERNAL_PORT}'
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

 

volume์„ ์ง€์ •ํ•˜์—ฌ docker-compose๊ฐ€ down ๋˜๋”๋ผ๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฌ๋ผ์ง€์ง€ ์•Š๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

7-6.  ๋ฐฐํฌ

๋ฐฐํฌ๋ฅผ ์œ„ํ•ด EC2 ์ธ์Šคํ„ด์Šค๋ฅผ Free Tier ์„ฑ๋Šฅ์œผ๋กœ ์ƒ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • ์šด์˜์ฒด์ œ: ubuntu 
  • ์ธ์Šคํ„ด์Šค ์œ ํ˜•: t2.micro
  • ์ง€์—ญ: ap-northeast-2

SSH๋ฅผ ํ†ตํ•ด ์ธ์Šคํ„ด์Šค์— ์ ‘์†ํ•˜์˜€์œผ๋ฉฐ, ํŽธ๋ฆฌํ•œ ์ ‘์†์„ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค์ •์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

[ .ssh/config ]

Host project
    HostName 54.180.182.40
    User ec2-user
    IdentityFile ~/key/ssh/project_key.pem

 

24์‹œ๊ฐ„ ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ๋ฅผ ํ•ด์•ผ ํ–ˆ๋Š”๋ฐ, Docker๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์•„๋ž˜์™€ ๊ฐ™์€ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ๊ฐ„๋‹จํžˆ ๋ฐฐํฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

[๋ฐฑ๊ทธ๋ผ์šด๋“œ ์‹คํ–‰]

docker-compose up --build -d

 

[์ข…๋ฃŒ]

docker-compose down

 


8. ์‹คํ–‰ ์‹œ๋‚˜๋ฆฌ์˜ค

8-1. ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ

  • ํšŒ์›๊ฐ€์ž…: ์ด๋ฆ„, ์•„์ด๋””, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, ์„ฑ๋ณ„, ์ƒ๋…„์›”์ผ์„ ์ž…๋ ฅํ•ด์•ผ ํšŒ์›๊ฐ€์ž…์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
    • ์ธ์ฆ๋ฒˆํ˜ธ ๋ฐœ์†ก์„ ํ†ตํ•ด์„œ ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ•ด์ค๋‹ˆ๋‹ค.
  • ๋กœ๊ทธ์ธ: ํšŒ์›๊ฐ€์ž… ํ›„, ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๋กœ๊ทธ์ธ์ด ๋ฉ๋‹ˆ๋‹ค.

8-2. ์š”๋ฆฌ

  • ์ž์ทจ์ƒ๋“ค์ด ๋ƒ‰์žฅ๊ณ ์— ์žˆ๋Š” ์žฌ๋ฃŒ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด AI๊ฐ€ ๋ ˆ์‹œํ”ผ๋ฅผ ์ถ”์ฒœํ•ด์ค๋‹ˆ๋‹ค.
  • AI๊ฐ€ ์ถ”์ฒœํ•ด์ค€ ๋ ˆ์‹œํ”ผ๋ฅผ ์‚ฌ์ง„๊ณผ ํ•จ๊ป˜ ์š”๋ฆฌ ๊ฒŒ์‹œํŒ์— ์˜ฌ๋ ค ๋‹ค๋ฅธ ์ž์ทจ์ƒ๋“ค๊ณผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๊ณ , ์กฐํšŒ์ˆ˜๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ๊ฒŒ์‹œ๊ธ€์€ ์ธ๊ธฐ ๋ ˆ์‹œํ”ผ BEST์— ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.
  • ๋˜ํ•œ, ์žฌ๋ฃŒ/์š”๋ฆฌ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋ฉด ํ•„ํ„ฐ๋ง ๋˜์–ด์„œ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

8-3. ์›๋ฃธ

  • ์›๋ฃธ api๋ฅผ ์ด์šฉํ•˜์—ฌ ๋‚ด ์ฃผ๋ณ€ ์›๋ฃธ๊ณผ ๊ทธ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ง€๋„๋กœ ํ™•์ธ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • ๋˜ํ•œ, ์กฐํšŒ์ˆ˜๊ฐ€ ๋งŽ์€ ์ˆœ์œผ๋กœ ๊ทธ ์ง€์—ญ์˜ ์ธ๊ธฐ ์›๋ฃธ Best 3๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

8-4. ์ž์ทจ์ƒ ๋ฉ”์ดํŠธ

  • ๋™๋„ค ์ž์ทจ์ƒ ์นœ๊ตฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€์ด๋ฉฐ, ํšŒ์›๊ฐ€์ž… ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ›์€ ์ž…๋ ฅ๊ฐ’์„ ๋ฐ”ํƒ•์œผ๋กœ ์นœ๊ตฌ๋ฅผ ์ถ”์ฒœํ•˜์—ฌ ์ง€์—ญ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.
  • ์ถ”์ฒœ ์ž์ทจ์ƒ ๋ฉ”์ดํŠธ ์™ธ์—๋„ ๋ณธ์ธ์ด ์›ํ•˜๋Š” ์กฐ๊ฑด์„ ํ•„ํ„ฐ๋งํ•˜์—ฌ ์ถ”์ฒœ ๋ฉ”์ดํŠธ์— ๋œจ์ง€ ์•Š๋Š” ์นœ๊ตฌ์™€๋„ ์—ฐ๋ฝ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • ๋ณธ์ธ์ด ์›ํ•˜๋Š” ์ž์ทจ์ƒ ์นœ๊ตฌ๋ฅผ ์ฐพ์•˜์œผ๋ฉด ์ชฝ์ง€๋ฅผ ์ฃผ๊ณ  ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

8-5. ์ค‘๊ณ ๋งˆ์ผ“

  • ์‚ฌ์šฉ์ž์™€ ๊ฐ™์€ ์ง€์—ญ์— ๊ฑฐ์ฃผํ•˜๋Š” ์œ ์ €๊ฐ€ ์˜ฌ๋ฆฐ ์ค‘๊ณ  ๋ฌผํ’ˆ์„ ๋ณผ ์ˆ˜ ์žˆ๊ณ , ์ชฝ์ง€๋ฅผ ํ†ตํ•ด ๊ฑฐ๋ž˜๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋˜ํ•œ, ๊ฒ€์ƒ‰์„ ํ†ตํ•ด ํ•„์š”ํ•œ ์ค‘๊ณ  ๋ฌผํ’ˆ์„ ์ฐพ์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์˜ฌ๋ฆฐ ๊ฒŒ์‹œ๊ธ€์„ ์ˆ˜์ •, ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๊ณ  ์ฐœํ•˜๊ธฐ๋ฅผ ํ†ตํ•ด ์›ํ•˜๋Š” ์ค‘๊ณ  ๋ฌผํ’ˆ์„ ์ฐœํ•ด๋†“์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

8-6. ์ปค๋ฎค๋‹ˆํ‹ฐ

  • ์ž์ทจ์ƒ๋“ค์—๊ฒŒ ํ•„์š”ํ•œ ์ •๋ณด๋“ค์„ ๋‹ค๋ฅธ ์ž์ทจ์ƒ๋“ค๊ณผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ฒŒ์‹œ๊ธ€์„ ์‚ฌ์ง„๊ณผ ํ•จ๊ป˜ ์˜ฌ๋ฆฌ๋ฉด ์˜ฌ๋ฆฐ ์‹œ๊ฐ„, ์กฐํšŒ์ˆ˜ ๋“ฑ์„ ๋ณผ ์ˆ˜ ์žˆ๊ณ , ์กฐํšŒ์ˆ˜๊ฐ€ ๊ฐ€์žฅ ๋งŽ์€ ๊ฒŒ์‹œ๊ธ€์€ ์ƒ๋‹จ์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋˜ํ•œ, ๊ณต๊ฐํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€์— ์ข‹์•„์š”์™€ ๋Œ“๊ธ€, ๋‹ต๊ธ€, ๋Œ“๊ธ€ ์ข‹์•„์š”๋ฅผ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๊ณ , ์ œ๋ชฉ์ด๋‚˜ ๋‚ด์šฉ์„ ๊ฒ€์ƒ‰ํ•ด์„œ ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

8-7. ์ชฝ์ง€

  • ์ค‘๊ณ ๋งˆ์ผ“๊ณผ ์ž์ทจ์ƒ ๋ฉ”์ดํŠธ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ค‘์— ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์™€ ์ชฝ์ง€๋ฅผ ์ฃผ๊ณ  ๋ฐ›์„ ์ˆ˜ ์žˆ๊ณ , ์ƒ๋Œ€๋ฐฉ์ด ๋‚ด ์ชฝ์ง€๋ฅผ ์ฝ์—ˆ๋Š”์ง€๋„ ํ™•์ธ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์ชฝ์ง€๊ฐ€ ๋‚˜๋ˆ ์ ธ์„œ ๋‚˜์˜ค๊ณ , ์†ก์‹ ํ•จ๊ณผ ์ˆ˜์‹ ํ•จ ๊ฐ๊ฐ ํ™•์ธ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

8-8. ์•Œ๋ฆผ

  • ์ข‹์•„์š”์™€ ๋Œ“๊ธ€, ์ชฝ์ง€ ๋“ฑ์„ ๋ฐ›๊ฑฐ๋‚˜ ๋‚ด๊ฐ€ ์ฐœํ•œ ๋ฌผํ’ˆ์˜ ๊ฐ€๊ฒฉ์ด ๋ณ€๋™๋˜๋Š” ๋“ฑ์˜ ์ƒํ™ฉ์ผ ๋•Œ, ์•Œ๋ฆผ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  • ์•Œ๋ฆผ์€ ์˜ค๋ž˜๋œ ์ˆœ์œผ๋กœ ๋ฐ‘์—์„œ๋ถ€ํ„ฐ ๋ฐฐ์—ด๋˜๊ณ , ๊ฐœ๋ณ„ ์‚ญ์ œ์™€ ์ „์ฒด ์‚ญ์ œ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

8-9. ํฌ์ธํŠธ

  • ์‚ฌ์šฉ์ž๋“ค์˜ ์ปค๋ฎค๋‹ˆํ‹ฐ ํ™œ๋™์— ๋™๊ธฐ๋ถ€์—ฌ๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ํฌ์ธํŠธ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๊ฒŒ ๋˜์—ˆ๊ณ , ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•˜๋Š” ๋“ฑ์˜ ํ™œ๋™์„ ํ•  ๊ฒฝ์šฐ์— ํฌ์ธํŠธ๋ฅผ ์ œ๊ณตํ•˜๋„๋ก ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ํฌ์ธํŠธ ๋ณ„๋กœ ๋“ฑ๊ธ‰์„ ๋งค๊ฒจ ์‚ฌ์ดํŠธ ์ด์šฉ์— ์žฌ๋ฏธ๋ฅผ ๋”ํ–ˆ์Šต๋‹ˆ๋‹ค.