๐Ÿฅ”Project/[Project] Threepark

[Threepark] ๋ฐฑ์—”๋“œ ์„œ๋ฒ„์™€ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„

mingyung 2024. 5. 21. 15:52

์ด๋ฒˆ ํฌ์ŠคํŒ…์€ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„ ๊ตฌ์ถ•๊ณผ์ •๊ณผ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„์— ๋Œ€ํ•ด ์•Œ์•„๋ณธ๋‹ค.

๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ณผ ์„œ๋ฒ„ ๊ตฌ์„ฑํ•˜๊ธฐ

๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์„ ์‹œ์ž‘ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๋ฐ๋ฒ  ์„œ๋ฒ„๋ฅผ ๋จผ์ € ์ƒ์„ฑํ•˜๋„๋ก ํ•œ๋‹ค.

RDS๋ฅผ ์ด์šฉํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ๋กœ ํ•˜์˜€๊ณ , MySQL์„ ์‚ฌ์šฉํ•œ๋‹ค.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

RDS์ƒ์„ฑ

AWS์— ํšŒ์›๊ฐ€์ž…์„ ์™„๋ฃŒํ•œ ์ƒํƒœ๋กœ ์‹œ์ž‘ํ•œ๋‹ค.

AWS ์ฝ˜์†”์˜ ์„œ๋น„์Šค์—์„œ RDS๋กœ ๋“ค์–ด๊ฐ„๋‹ค. ์ด๋•Œ ์ƒ๋‹จ์˜ region์„ ์„œ์šธ๋กœ ๋ฐ”๊ฟ”์•ผ ํ•œ๋‹ค.

์ด์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ์„ ๋ˆŒ๋Ÿฌ RDS์ธ์Šคํ„ด์Šค ์ƒ์„ฑ์„ ์‹œ์ž‘ํ•œ๋‹ค.

์ด ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด์ž!

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ ๊ณผ์ •

ํ‘œ์ค€ ์ƒ์„ฑ์„ ์„ ํƒํ•œ๋‹ค.

์šฐ๋ฆฌ๋Š” MySQL์„ ์‚ฌ์šฉํ•  ์˜ˆ์ •์ด๋ฏ€๋กœ MySQL์„ ํƒ.

๋ฐ”๋กœ ์•„๋ž˜ ์—”์ง„ ๋ฒ„์ „์„ ํ™•์ธํ•˜์ž

์šฐ๋ฆฌ๋Š” ํ”„๋ฆฌํ‹ฐ์–ด๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋ฏ€๋กœ ํ”„๋ฆฌํ‹ฐ์–ด ์„ ํƒ.

์‚ฌ์šฉํ•  RDS ์ธ์Šคํ„ด์Šค์˜ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜๊ณ , ๋งˆ์Šคํ„ฐ ์‚ฌ์šฉ์ž๋ฅผ ์„ค์ •ํ•œ๋‹ค. ๋งˆ์Šคํ„ฐ ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ์•”ํ˜ธ๋Š” ๊ผญ ์žŠ์ง€๋ง์ž!!

ํ”„๋ฆฌํ‹ฐ์–ด์—์„œ ์Šคํ† ๋ฆฌ์ง€๋Š” 20GiB๊นŒ์ง€ ์“ธ ์ˆ˜ ์žˆ๋‹ค.

์•„๋ž˜์—์„œ ํผ๋ธ”๋ฆญ ์•ก์„ธ์Šค๋ฅผ '์˜ˆ'๋กœ ๋ฐ”๊ฟ”์ฃผ์ž.

VPC๋ณด์•ˆ๊ทธ๋ฃน์„ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค. ๋งŒ์•ฝ ๊ธฐ์กด ๊ทธ๋ฃน์ด ์žˆ๋‹ค๋ฉด ์ „์ž๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ์ž. ๋งŒ์•ฝ ์ฒ˜์Œ ์‚ฌ์šฉํ•˜๋Š”๊ฑฐ๋ผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ์„ ์„ ํƒํ•œ๋‹ค.

์ƒˆ๋กœ ์ƒ์„ฑ์„ ๋ˆŒ๋ €๋‹ค๋ฉด ์ƒˆ VPC ๋ณด์•ˆ ๊ทธ๋ฃน ์ด๋ฆ„์— ์ด๋ฆ„์„ ์ •ํ•ด์ค€๋‹ค.

์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์ด๋ฆ„์„ ์ •ํ•ด์ฃผ์ž

์ด์ œ ๊ธฐํƒ€ ์กฐ๊ฑด๋“ค์„ ์ž˜ ์ฝ์–ด๋ณด๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ์„ ์„ ํƒํ•ด ์ƒ์„ฑ ์™„๋ฃŒํ•œ๋‹ค.

์ธ๋ฐ”์šด๋“œ ๊ทœ์น™ ํŽธ์ง‘

์œ„์˜ ๊ณผ์ •์„ ํ†ตํ•ด์„œ RDS์ƒ์„ฑ์„ ์™„๋ฃŒํ–ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ง€๊ธˆ ์ด ์ƒํƒœ๋กœ๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

RDS์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ธ๋ฐ”์šด๋“œ ๊ทœ์น™์„ ์„ค์ •ํ•ด์ค˜์•ผ ํ•œ๋‹ค. ์ธ๋ฐ”์šด๋“œ ๊ทœ์น™์„ ํ†ตํ•ด ํ—ˆ์šฉ๋œ IP์ฃผ์†Œ, ํฌํŠธ์—์„œ ์ด RDS์— ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•œ๋‹ค.

๋จผ์ € ๋Œ€์‹œ๋ณด๋“œ ์•„๋ž˜์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ƒ์„ฑํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

์ด๋ ‡๊ฒŒ ์—ฐ๊ฒฐ ๋ฐ ๋ณด์•ˆ์„ ๋ณด๋ฉด VPC ๋ณด์•ˆ ๊ทธ๋ฃน์ด ํ‘œ์‹œ ๋œ๋‹ค. ํ•ด๋‹น ๋ณด์•ˆ๊ทธ๋ฃน์œผ๋กœ ๋“ค์–ด๊ฐ€ ์ธ๋ฐ”์šด๋“œ ๊ทœ์น™์„ ํŽธ์ง‘ํ•œ๋‹ค.

์ธ๋ฐ”์šด๋“œ ๊ทœ์น™ ํŽธ์ง‘์„ ๋ˆŒ๋Ÿฌ MySQL/Aurora, Port(3306)์— Anywhere-IPv4,Anywhere-IPv6์„ ๋ชจ๋‘ ์—ด์–ด์ค€๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€ ์™„๋ฃŒํ•˜๋ฉด RDS์„ธํŒ…์€ ๋๋‚œ๋‹ค.

์ด์ œ MySQL์„ RDS์™€ ์—ฐ๊ฒฐํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํด๋ผ์šฐ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณธ๋‹ค.

MySQL ๋‹ค์šด๋กœ๋“œ

https://dev.mysql.com/downloads/installer/

[MySQL :: Download MySQL Installer

Note: MySQL 8.0 is the final series with MySQL Installer. As of MySQL 8.1, use a MySQL product's MSI or Zip archive for installation. MySQL Server 8.1 and higher also bundle MySQL Configurator, a tool that helps configure MySQL Server.

dev.mysql.com](https://dev.mysql.com/downloads/installer/)

MySQL์„ ๋‹ค์šด๋ฐ›์ž

๋ฒ„์ „์€ 8.0์œผ๋กœ ํ•œ๋‹ค.

๋‘๊ฐœ์˜ ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ ์ค‘ ๋‘๋ฒˆ์งธ๋ฅผ ๋ˆ„๋ฅธ๋‹ค.

์ดํ›„ ์•ˆ๋‚ด๋ฅผ ๋”ฐ๋ผ MySQL์„ ์„ค์น˜ํ•˜๋„๋ก ํ•œ๋‹ค.

MySQL๊ณผ RDS ์—ฐ๊ฒฐ

์„ค์น˜ ์™„๋ฃŒ ํ›„ MySQL Workbench๋ฅผ ์—ฐ๋‹ค.

๊ฒ€์ƒ‰ํ•˜์—ฌ ์ฐพ์•„์ฃผ์ž

์ด์ œ MySQL Connections ์˜†์˜ +๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ๋‹ค

๊ทธ๋Ÿผ ์ด๋Ÿฐ ์ฐฝ์ด ๋œจ๊ฒŒ๋œ๋‹ค.

๋จผ์ € Connection Name์€ ์–ด๋–ค MySQL์— ๋Œ€ํ•œ ์—ฐ๊ฒฐ์ธ์ง€ ์ธ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์ด๋ฆ„์ด๋ฏ€๋กœ ๋ณธ์ธ์ด ์›ํ•˜๋Š” ์ด๋ฆ„์œผ๋กœ ์ž‘์„ฑํ•œ๋‹ค.

Hostname์—๋Š” RDS์˜ ์—”๋“œํฌ์ธํŠธ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.

์ด์ „์— ์ƒ์„ฑํ•œ RDS์— ๋“ค์–ด๊ฐ€๋ฉด ์—”๋“œํฌ์ธํŠธ์— ๋Œ€ํ•œ ์ •๋ณด๊ฐ€ ์žˆ์œผ๋‹ˆ ๋ณต์‚ฌํ•˜์—ฌ ๋ถ™์—ฌ๋„ฃ๋Š”๋‹ค.

Port์—๋Š” ํฌํŠธ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.

๊ธฐ๋กํ•ด๋’€๋˜ username์„ ์ ๊ณ , store in vault๋ฅผ ๋ˆŒ๋Ÿฌ password๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.

Test Connection๋ฅผ ๋ˆ„๋ฅด๊ณ  ์•„๋ž˜ ์•Œ๋žŒ์ด ๋œฌ๋‹ค๋ฉด Ok๋ฅผ ๋ˆ„๋ฅธ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™˜๊ฒฝ ๊ตฌ์„ฑ์ด ์™„๋ฃŒ๋œ๋‹ค.

์Šคํ† ๋ฆฌ์ง€ ์„œ๋น„์Šค

๊ตฌํ˜„ํ•˜๋ ค๋Š” ์„œ๋น„์Šค๋Š” ํŠœ๋‹๋œ ์ƒ์„ฑ ๋ชจ๋ธ์„ ํ™œ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ฃผ์š” ๊ธฐ๋Šฅ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•  ๊ณณ์ด ํ•„์š”ํ•˜๋‹ค.

๋”ฐ๋ผ์„œ AWS์˜ S3๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. ํ”„๋ฆฌํ‹ฐ์–ด ๋งŒ์„ธ

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” AWS์˜ S3๋ฒ„ํ‚ท์„ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๊ธฐ๋กํ•œ๋‹ค.

AWS S3 ์ƒ์„ฑ

RDS์ƒ์„ฑ๋•Œ์™€ ๊ฐ™์ด AWS ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์‹œ์ž‘ํ•œ๋‹ค. ๋ฆฌ์ „์ด ์„œ์šธ๋กœ ๋˜์–ด์žˆ๋Š”๊ฒƒ์„ ํ•œ๋ฒˆ ๋” ํ™•์ธํ•˜์ž.

AWS์ฝ˜์†”์˜ ์ƒ๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ”์—์„œ ์„œ๋น„์Šค ํด๋ฆญํ•˜์—ฌ S3๋ฅผ ์ฐพ์ž.

๋ฒ„ํ‚ท ๋งŒ๋“ค๊ธฐ ํด๋ฆญ

RDS์ƒ์„ฑ ๋•Œ ๋ณด๋‹ค ๊ฐ„๋‹จํ•˜๋‹ค.

๋ฒ„ํ‚ท์„ ๊ตฌ๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ์ด๋ฆ„์„ ์ž…๋ ฅํ•œ๋‹ค.

ํ”„๋ก ํŠธ๋‚˜ ๋ฐฑ์—”๋“œ์—์„œ ๋ฒ„ํ‚ท์— ์ ‘๊ทผํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด์„œ๋Š” "๋ชจ๋“  ํผ๋ธ”๋ฆญ ์•ก์„ธ์Šค ์ฐจ๋‹จ"์„ ํ’€์–ด์•ผ ํ•œ๋‹ค.

๋Œ€์‹  ์•„๋ž˜ ์‚ฌ์ง„ ์ฒ˜๋Ÿผ ์„ ํƒํ•˜๊ณ , ์ถ”ํ›„์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์•ก์„ธ์Šค ์ง€์ ์„ ์ •์˜ํ• ๊ฒƒ์ด๋‹ค.

์•”ํ˜ธํ™” ์„ค์ •์„ ํ™•์ธํ•˜๊ณ  ์ƒ์„ฑ ์™„๋ฃŒ ํ•œ๋‹ค

์™„๋ฃŒ ํ›„ ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ”์˜ ๋ฒ„ํ‚ท์œผ๋กœ ์ด๋™ํ•˜๋ฉด ์ƒ์„ฑ๋œ ๋ฒ„ํ‚ท์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ด ์ƒํƒœ๋กœ๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

์™ธ๋ถ€์—์„œ ์ด ๋ฒ„ํ‚ท์— ์ ‘๊ทผํ•ด ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•˜๊ณ , ์ฝ๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ ‘๊ทผ ๊ถŒํ•œ์„ ์„ค์ •ํ•ด์ค˜์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ด๋ฒˆ์—๋Š” CORS์„ค์ •๊ณผ ๋ฒ„ํ‚ท ์ •์ฑ…์„ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค

๋ฒ„ํ‚ท ์ •์ฑ… ์„ค์ •

์ƒ์„ฑ๋œ ๋ฒ„ํ‚ท์œผ๋กœ ๋“ค์–ด๊ฐ€์„œ ๋ฒ„ํ‚ท์˜ ๊ถŒํ•œ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค

๊ถŒํ•œ์„ ํด๋ฆญํ•˜๋ฉด ์ •์ฑ…์„ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ •์ฑ… ํŽธ์ง‘์„ ๋ˆ„๋ฅธ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ๋ฒ„ํ‚ท ARN์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ๋ณต์‚ฌํ•˜๊ณ , ์ •์ฑ… ์ƒ์„ฑ๊ธฐ๋ฅผ ํด๋ฆญํ•œ๋‹ค.

1. Select Type of Policy ์—์„œ S3 Bucket Policy๋ฅผ ์„ ํƒํ•œ๋‹ค.

2. Principal์— * ์ž…๋ ฅ

3. Actions์— Get Object, Put Object ์„ ์ฒดํฌํ•œ๋‹ค.

4. Amazon Resource Name (ARN) ์— ์œ„์—์„œ ๋ณต์‚ฌํ•œ ARN์„ ์ž…๋ ฅํ•œ ํ›„ /* ์ž…๋ ฅ ex)arn:aws:s3:::ARN๋ณต์‚ฌํ•œ๊ฒƒ/*

5. Add Statement ํด๋ฆญํ•œ๋‹ค

์ƒ์„ฑ์„ ์™„๋ฃŒํ•˜๋ฉด, policy json document ๊ฐ€ ๋‚˜์˜ค๋Š”๋ฐ, ์ด๋ฅผ ๋ณต์‚ฌํ•œ๋‹ค.

์ด์ „์˜ ์ •์ฑ… ํŽธ์ง‘ ํŽ˜์ด์ง€๋กœ ๋Œ์•„์‚ฌ์„œ ์ •์ฑ…ํ•œ์— json๊ตฌ๋ฌธ์„ ๋ถ™์—ฌ๋„ฃ๊ธฐ ํ•˜๊ณ  ์ €์žฅํ•œ๋‹ค.

CORS

๋ฒ„ํ‚ท ์ •์ฑ… ์•„๋ž˜์˜ CORS๋ฅผ ์„ค์ •ํ•œ๋‹ค.

ํŽธ์ง‘์„ ๋ˆŒ๋Ÿฌ ์•„๋ž˜๋ฅผ ๋ถ™์—ฌ๋„ฃ๋Š”๋‹ค.

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "HEAD",
            "GET",
            "PUT",
            "POST"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

๊ณต๊ฐœ ์„ค์ •

์œ„์˜ ๊ณผ์ •์œผ๋กœ ๋ฒ„ํ‚ท์˜ ์ •์ฑ…์— ๋Œ€ํ•œ ์„ค์ •์„ ์™„๋ฃŒํ–ˆ์ง€๋งŒ, ๊ทธ๋ ‡๋‹ค๊ณ  ํ•ด์„œ ๋ฒ„ํ‚ท์˜ ์ด๋ฏธ์ง€๋ฅผ ์™ธ๋ถ€์—์„œ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š”๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.

์ด๋ฒˆ์—๋Š” ์ด๋ฏธ์ง€๊ฐ€ ์ €์žฅ๋œ images ํด๋”๋งŒ ์™ธ๋ถ€์—์„œ ์ด๋ฏธ์ง€ url๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์ž.

๋จผ์ € ๋ฒ„ํ‚ท์œผ๋กœ ๋“ค์–ด์™€์„œ ํด๋” ๋งŒ๋“ค๊ธฐ๋ฅผ ํ†ตํ•ด์„œ images ํด๋”๋ฅผ ๋งŒ๋“ ๋‹ค. ํฐ๋”๊ฐ€ ์•„๋ž˜์ฒ˜๋Ÿผ ์ƒ์„ฑ๋˜์—ˆ๋‹ค๋ฉด ๋‹ค์Œ์œผ๋กœ ๋„˜์–ด๊ฐ€์ž

ํด๋” ์™ผ์ชฝ์˜ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํด๋ฆญํ•˜๊ณ , ์œ„์˜ ์ž‘์—…์„ ๋ˆ„๋ฅด๋ฉด ACL์„ ์‚ฌ์šฉํ•˜์—ฌ ํผ๋ธ”๋ฆญ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด์ œ ์ด ํด๋”์˜ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์™ธ๋ถ€์—์„œ url์„ ํ†ตํ•ด ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

ํ™•์ธ์„ ์œ„ํ•ด ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ๊ณ ,

url์ฐฝ์— ์ด๋ฏธ์ง€ url์„ ๋„ฃ์œผ๋ฉด ๋‹ค์Œ์ฒ˜๋Ÿผ ๋‚˜์˜จ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€ ํ•˜๋ฉด ์Šคํ† ๋ฆฌ์ง€ ๊นŒ์ง€ ์ค€๋น„๊ฐ€ ์™„๋ฃŒ๋œ๋‹ค.

๋ฐฑ์—”๋“œ - Django Rest Framework

์ด์ œ๋Š” Django - DRF์™€ Flask๋ฅผ ์ด์šฉํ•ด์„œ ๋ฐฑ์•ค๋“œ API์™€ ์ƒ์„ฑํ˜•, ๋ถ„๋ฅ˜ ๋ชจ๋ธ์„ ์—ฐ๊ฒฐํ•˜๋„๋ก ํ•œ๋‹ค.

๋ฐฑ์—”๋“œ API ๊ตฌํ˜„์„ ์‹œ์ž‘ํ•˜๋„๋ก ํ•œ๋‹ค.

์šฐ๋ฆฌ ์„œ๋น„์Šค์—์„œ๋Š” ์žฅ๊ณ ์™€ DRF๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ•˜์˜€๋‹ค. DRF ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ํ™˜๊ฒฝ์„ค์ •์„ ํ•˜๋Š” ๊ณผ์ •์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž.

Django Rest Framework ํ™˜๊ฒฝ ์„ค์ •

DRF ์„ค์น˜

์ผ๋‹จ ํŒŒ์ด์ฌ ์‚ฌ์šฉ์„ ์ „์ œ๋กœ ํ•œ๋‹ค.

ํŒŒ์ด์ฌ ๋ฒ„์ „์€ 3.10์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

pip install django
pip install djangorestframework

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ์„ ์›ํ•˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ๋‹ค์Œ์„ ์‹คํ–‰ํ•œ๋‹ค.

django-admin startproject ํ”„๋กœ์ ํŠธ์ด๋ฆ„ .

์ด๋ฅผ ์™„๋ฃŒํ•˜๋ฉด djnago ํ”„๋กœ์ ํŠธ๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.

app์ƒ์„ฑ

๊ฐœ๋ณ„ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” app๋Š˜ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” ์•„๋ž˜์˜ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

python manage.py startapp APP์ด๋ฆ„

์˜ˆ) python manage.py startapp diary

Settings.py ์„ค์ •

settings.py ์— ๋“ค์–ด๊ฐ€๋ฉด INSTALLED_APPS๊ฐ€ ์žˆ๋‹ค. ๊ฑฐ๊ธฐ์— ๋‹ค๋ฆ„์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

์ถ”๊ฐ€ ํ›„ ์ „์ฒด ์ €์žฅ์„ ํ•œ ๋’ค ๋‹ค์Œ์„ ์‹คํ–‰ํ•œ๋‹ค.

python manage.py migrate

๊ฐœ๋ฐœ์šฉ ๋กœ์ปฌ ์„œ๋ฒ„ runserver

ํ˜„์žฌ ๊ฐœ๋ฐœ์— ๋Œ€ํ•œ ๊ตฌํ˜„์„ ๋กœ์ปฌ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

python manage.py runserver

Django Environ์œผ๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ด€๋ฆฌํ•˜๊ธฐ

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ์€ ๋๋‚ฌ๊ณ , ์ด์ œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ .envํŒŒ์ผ์„ ๊ตฌ์„ฑํ•ด์•ผ ํ•œ๋‹ค.

DRF๋ฅผ ์ด์šฉํ•ด ์„œ๋น„์Šค๋ฅผ ๊ฐœ๋ฐœํ•˜๊ธฐ ์œ„ํ•ด์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•  ํ™˜๊ฒฝ๋ณ€์ˆ˜๋“ค์„ Django Environ์„ ํ†ตํ•ด ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

ํŠนํžˆ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋‚˜ APIํ‚ค ๋“ฑ ์™ธ๋ถ€๋กœ ์œ ์ถœ๋˜๋ฉด ์•ˆ๋˜๋Š” ์ •๋ณด๋Š” ์ด๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.

์„ค์น˜

pip install django-environ

.env ์ƒ์„ฑ

ํ”„๋กœ์ ํŠธ์˜ ๋ฃจํŠธ์— .env ํŒŒ์ผ์„ ๋จผ์ € ์ƒ์„ฑํ•ด์ค€๋‹ค.

์ƒ์„ฑ ํ›„์—๋Š” ๊ผญ gitignoreํŒŒ์ผ์— .env๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊นƒํ—ˆ๋ธŒ์— ์˜ฌ๋ผ๊ฐ€์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.

install django-environ

๋‹ค์Œ์„ ํ„ฐ๋ฏธ๋„์— ์ž…๋ ฅํ•˜์—ฌ django-environ์„ ์„ค์น˜ํ•œ๋‹ค.

pip install django-environ

.env ์ž‘์„ฑ

ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ง€์ •ํ•ด์•ผ ํ•˜๋Š”๊ฐ’๋“ค์„ env์— ์ •์˜ํ•œ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค.

SECRET_KEY='django-insecure-...'
DEBUG=True
# MySQL DB
DB_NAME='localdb'
...

์ด๋•Œ ์ฃผ์˜ํ•  ์ ์€ ํ‚ค์™€ ๊ฐ’ ์‚ฌ์ด์— ๋„์–ด์“ฐ๊ธฐ๋ฅผ ํฌํ•จํ•ด์„œ๋Š” ์•ˆ๋œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

# ํ‹€๋ฆฐ ์˜ˆ์‹œ
DB_NAME = 'localdb'

# ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ์‹œ
DB_NAME='localdb'

settings.py ์ž‘์„ฑ

settings.py์— ๋‹ค์Œ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

import environ
...
env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
...


# ์ด์ œ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ๋“ค์–ด๊ฐˆ ์ž๋ฆฌ๋ฅผ ๋‹ค์Œ์ฒ˜๋Ÿผ ๋ฐ”๊พธ์–ด ์ž‘์„ฑํ•ด์ค€๋‹ค.
SECRET_KEY = env('SECRET_KEY')
DEBUG = env('DEBUG')
...

migrate

settings.py์ž‘์„ฑ์„ ๋งˆ์ณค๋‹ค๋ฉด migrate๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.

python manage.py migrate

Django์— RDS ์—ฐ๊ฒฐํ•˜๊ธฐ

์ด๋ฒˆ์—๋Š” django ์™€ AWS RDS๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณธ๋‹ค.

์„ค์น˜

pip install boto3
pip install mysqlclient

Settings.py

ํ”„๋กœ์ ํŠธ์˜ settings.py์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•œ๋‹ค.

๊ฐœ๋ณ„ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋“ค์€ ์ด์ „ ํฌ์ŠคํŒ…์„ ํ†ตํ•ด์„œ ์ƒ์„ฑํ•œ .envํŒŒ์ผ์— ์ •์˜ํ•œ๋‹ค.

๋ชจ๋‘ ์ €์žฅํ•˜๊ธฐ๋ฅผ ํ•œ๋ฒˆ ๋ˆ„๋ฅด๊ณ ,

migration์„ ์ง„ํ–‰ํ•œ๋‹ค.

python manage.py migrate

DRF ๋ฐฑ์—”๋“œ ๊ตฌํ˜„

๋“œ๋””์–ด ๋ฐฑ์—”๋“œ์˜ ๊ตฌํ˜„์ด ์‹œ์ž‘๋œ๋‹ค!! ๋ชจ๋“  ๊ตฌํ˜„๊ณผ์ •์„ ์„ค๋ช…ํ•  ์ˆ˜๋Š” ์—†๊ธฐ ๋•Œ๋ฌธ์— ๊ตฌํ˜„ ์ƒ ํŠน์ˆ˜ํ•œ ๋ถ€๋ถ„๊ณผ ํ•ต์‹ฌ ๊ธฐ๋Šฅ๋ถ€๋ถ„์„ ํฌ์ŠคํŒ…ํ•œ๋‹ค.

๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ง„ํ–‰๋œ๋‹ค.

1. Model ๋งŒ๋“ค๊ธฐ

2. Serializer ๋งŒ๋“ค๊ธฐ

3. permissions ์ปค์Šคํ…€ํ•˜์—ฌ ์ ‘๊ทผ ๊ถŒํ•œ ๋งŒ๋“ค๊ธฐ

4. views ์ž‘์„ฑํ•˜๊ธฐ

Authentication์ด ๊ถ๊ธˆํ•˜๋‹ค๋ฉด ์•„๋ž˜ ํฌ์ŠคํŒ…์„ ์‚ดํŽด๋ณผ ์ˆ˜ ์žˆ๋‹ค. (ํ•˜์ง€๋งŒ ์ด ํฌ์ŠคํŒ…์—์„œ๋Š” ๋‹ค๋ฃจ์ง€ ์•Š๋Š”๋‹ค)

https://he-kate1130.tistory.com/61

[[Team 22] DRF Authentication - dj-rest-auth

์†Œ์…œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ์ถ”ํ›„์— ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด์„œ dj-rest-auth๋ฅผ ์‚ฌ์šฉํ•ด ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณด๋„๋ก ํ•˜์ž. ์‚ฌ์‹ค ์›๋ž˜๋Š” simple jwt๋ฅผ ํ™œ์šฉํ•ด์„œ ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๊ณ  ์ฟ ํ‚ค์— ์ €์žฅํ•ด๋‘๋Š” ๋ฐฉ์‹์œผ

he-kate1130.tistory.com](https://he-kate1130.tistory.com/61)

DB & MODEL

์ด์ œ DB Schema๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ , django์˜ Model์„ ์ž‘์„ฑํ•˜์ž

DB Schema ๊ตฌ์„ฑ

DB ์Šคํ‚ค๋งˆ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑํ•œ๋‹ค.

๊ณ„ํš์˜ ๊ณผ์ •์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ๊ตฌ์ƒํ–ˆ๋‹ค. (์ด๋ฏธ์ง€ ์ž์ฒด๋Š” ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€๋‹ค)

- ์œ ์ € ์ •๋ณด์˜ ๊ฒฝ์šฐ django์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ•จ

๋ชจ๋ธ์ด ๋งŽ์€ ํŽธ์ด๋ฏ€๋กœ user, follow, diary, music, image, emotion์˜ ๊ฒฝ์šฐ๋งŒ ์ด์•ผ๊ธฐ ํ•ด ๋ณธ๋‹ค.

User

์œ ์ €๋ชจ๋ธ์€ django์˜ auth๋ชจ๋ธ์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋‹ค. ๋”ฐ๋ผ์„œ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•œ๋‹ค.

from django.db import models
from django.contrib.auth.models import AbstractUser, BaseUserManager

class UserManager(BaseUserManager):
    def create_user(self, email, username, password=None, **extra_fields):
        if not email:
            raise ValueError('The Email field must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, username=username, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, username, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        return self.create_user(email, username, password, **extra_fields)

class User(AbstractUser):
    REQUIRED_FIELDS = ['email'] 
    objects = UserManager()

    class Meta:
        db_table = 'user'

    def get_by_natural_key(self, username):
        return self.get(username=username)

Follow

follow๋ชจ๋ธ์˜ ๊ฒฝ์šฐ ์œ ์ € ๋‘๋ช…๊ณผ ๋‘๋ช…์˜ ๊ด€๊ณ„๊ฐ€ ์ •์˜๋œ status๊ฐ’์ด ๋“ค์–ด๊ฐ„๋‹ค.

์ด๋•Œ ์œ ์ € ๋‘๋ช…์€ ์œ ๋‹ˆํฌํ•œ ์Œ์ด ๋˜์–ด์•ผ ํ•œ๋‹ค. (์œ ์ €๊ฐ„์— ์—ฌ๋Ÿฌ๊ฐœ์˜ ๊ด€๊ณ„๊ฐ€ ์žˆ์„ ์ˆ˜ ์—†๋‹ค.)

from django.db import models

class Follow(models.Model):
    REQUESTED = 'requested'
    ACCEPTED = 'accepted'
    REJECTED = 'rejected'
    STATUS_CHOICES = (
        (REQUESTED, 'Requested'),
        (ACCEPTED, 'Accepted'),
        (REJECTED, 'Rejected'),
    )

    follower = models.ForeignKey(User, related_name='following', on_delete=models.CASCADE)
    following_user = models.ForeignKey(User, related_name='followers', on_delete=models.CASCADE)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=REQUESTED)

    class Meta:
        unique_together = ('follower', 'following_user')
        db_table = 'follow'
        managed = True

Music

์Œ์•… ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” Music์˜ ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•œ๋‹ค.

from django.db import models

class Music(models.Model):
    music_title = models.CharField(max_length=100, null=True)
    artist = models.CharField(max_length=100, null=True)
    genre = models.CharField(max_length=20, null=True)

    class Meta:
        managed = True
        db_table = 'music'

Diary

์Œ์•… ์ •๋ณด์™€ ์œ ์ €๋ฅผ fk๋กœ ๊ฐ€์ง€๋Š” diary์˜ ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

ํƒ€์ดํ‹€๊ณผ ๋‚ด์šฉ(content), ์ตœ์ดˆ ์ƒ์„ฑ ์‹œ๊ฐ„๊ณผ ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„์„ ์ €์žฅํ•˜๊ณ , ํŒ”๋กœ์›Œ์—๊ฒŒ ๊ณต๊ฐœ์—ฌ๋ถ€๋ฅผ ์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

class Diary(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    music = models.ForeignKey(Music, on_delete=models.SET_NULL, blank=True, null=True)

    title = models.CharField(max_length = 30)
    content = models.TextField(blank=True)
    registered_at = models.DateTimeField(auto_now_add=True)
    last_update_at = models.DateTimeField(auto_now=True)
    is_open = models.BooleanField(default=False)

    class Meta: 
        managed = True
        db_table = 'diary'

Image

์ผ๊ธฐ๋ฅผ fk๋กœ ๊ฐ€์ง€๊ณ , ์ด๋ฏธ์ง€์˜ url์„ ๋‹ค๋ฃจ๋Š” ๋ชจ๋ธ๋กœ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ชจ๋ธ์„ ๊ตฌ์„ฑํ•œ๋‹ค.

class Image(models.Model):
    diary = models.ForeignKey(Diary, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    image_url = models.URLField(null=True)
    image_prompt = models.TextField(null=True)
    class Meta:
        managed = True
        db_table = 'image'

Emotion

์ผ๊ธฐ๋ฅผ fk๋กœ ๊ฐ€์ง€๊ณ , emotion_label, chat์„ ๋‹ค๋ฃจ๋Š” ๋ชจ๋ธ

class Emotion(models.Model):
    diary = models.ForeignKey(Diary,on_delete=models.CASCADE)
    emotion_label = models.CharField(max_length=10, blank=True)
    emotion_prompt = models.TextField(blank=True)
    chat = models.TextField(blank=True)
    class Meta:
        db_table ='emotion'

migration

์ „์ฒด ๋ชจ๋ธ์„ ์ €์žฅํ•˜๊ณ , ๋‹ค์Œ์„ ์ˆœ์ฐจ ์‹คํ–‰ํ•œ๋‹ค.

python manage.py makemigrations
python manage.py migrate

์—ฌ๊ธฐ๊นŒ์ง€ํ•ด์„œ ๋ชจ๋ธ๊ตฌ์„ฑ์„ ํ†ตํ•œ DB ์Šคํ‚ค๋งˆ ๊ตฌ์„ฑ์ด ๋๋‚œ๋‹ค.

SERIALIZER

๋ชจ๋ธ ์ž‘์„ฑ์„ ์™„๋ฃŒ ํ›„ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ €๋ฅผ ์ž‘์„ฑํ•˜๋„๋ก ํ•œ๋‹ค.

์‹œ๋ฆฌ์–ผ๋ผ์ด์ €์˜ ๊ฒฝ์šฐ ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š”๊ฒฝ์šฐ ๋Œ€์ฒด๋กœ all๋กœ ์ž‘์„ฑํ•˜์˜€์œผ๋ฉฐ, ํŠน์ˆ˜ํ•œ ๊ฒฝ์šฐ์ธ community์™€ diary๋งŒ ์‚ดํŽด๋ณธ๋‹ค.

Diary

์ผ๊ธฐ์˜ ๊ฒฝ์šฐ ํฌ๊ฒŒ ์ผ๊ธฐ ๋‚ด์šฉ๊ณผ, ์ผ๊ธฐ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ƒ์„ฑ๋œ ์Œ์•…, ์ด๋ฏธ์ง€, ๊ฐ์ • ๋ฐ์ดํ„ฐ๋กœ ํฌ๊ฒŒ ๋‚˜๋ˆ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

์ด์ค‘ ์Œ์•…์˜ ๊ฒฝ์šฐ๋Š” diary๋ชจ๋ธ์—์„œ fk๋กœ ์ฐธ์กฐํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์Œ์•… ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ผ๊ธฐ์™€ ์—ฐ๊ฒฐํ•˜๋Š” ๋ถ€๋ถ„์€ ๋”ฐ๋กœ ๊ตฌ์„ฑํ–ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ €๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

1. ์ˆœ์ˆ˜ํ•˜๊ฒŒ ์ผ๊ธฐ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ณ , ๋‹ค๋ฅธ ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ๋Š” only read๋กœ ์ฝ์„์ˆ˜๋งŒ ์žˆ๋„๋ก ํ•˜๋Š” DiarySerializer

2. ์ผ๊ธฐ ๋‚ด์šฉ์„ ํ† ๋Œ€๋กœ ์Œ์•…์„ ์ถ”์ฒœํ•˜๊ณ , ์Œ์•…-์ผ๊ธฐ๋ฅผ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” DiaryMusicSerializer

DiarySerializer

class DiarySerializer(serializers.ModelSerializer):
    music = MusicSerializer(required=False)
    image_set = ImageSerializer(many=True, read_only=True)
    emotion_set = EmotionSerializer(many=True, read_only=True)

    class Meta:
        model = Diary
        fields = ['id','user','title','content','registered_at','last_update_at','music','is_open','image_set','emotion_set']

DiaryMusicSerializer

class DiaryMusicSerializer(serializers.ModelSerializer):
    music = MusicSerializer(required=False)

    class Meta:
        model = Diary
        fields =['id', 'user', 'content', 'music']

    def update(self, instance, validated_data):
        music_data = validated_data.pop('music', None)
        instance = super().update(instance, validated_data)

        if music_data:
            music, _ = Music.objects.get_or_create(**music_data)
            instance.music = music

        return instance

Community

์ปค๋ฎค๋‹ˆํ‹ฐ์˜ ๊ฒฝ์šฐ ์„œ๋กœ ํŒ”๋กœ์šฐ๊ฐ€ ํ—ˆ์šฉ๋œ ๊ด€๊ณ„์˜ ์œ ์ € ์ผ๊ธฐ ์ค‘ ๊ณต๊ฐœ๋œ ๊ฒƒ๋“ค์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

CommunitySerializer

class CommunitySerializer(serializers.ModelSerializer):
    user = UserSerializer(required = False)
    music = MusicSerializer(required=False)
    image_set = ImageSerializer(many=True, read_only=True)

    class Meta:
        model = Diary
        fields = ['id','user','title','content','music','image_set','registered_at','last_update_at', 'is_open']

์—ฌ๊ธฐ์— ํŒ”๋กœ์šฐ ์ •๋ณด๊ฐ€ ์—†๋Š”๋ฐ์š”? > ์ด๋ถ€๋ถ„์€ ํ›„์— permissions๋ฅผ ํ†ตํ•ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.

PERMISSION

๋ทฐ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ์— ์•ž์„œ์„œ ์ ‘๊ทผ์˜ ์ œ์–ด๋ฅผ ์œ„ํ•œ ์ปค์Šคํ…€ permissions๋ฅผ ์ž‘์„ฑํ•˜์ž.

permissions.py

๋จผ์ € permissions.py๋ฅผ settings.py๊ฐ€ ์žˆ๋Š” ํด๋”์— ์ƒ์„ฑํ•œ๋‹ค.

์ด ํด๋”์— ์ปค์Šคํ…€ permissions๋ฅผ ์ž‘์„ฑํ•  ๊ฒƒ์ด๋‹ค.

import

permissions.py์˜ ์ƒ๋‹จ์— ๋‹ค์Œ์„ import ํ•ด์•ผ ํ•œ๋‹ค.

from rest_framework import permissions

Permission ์ž‘์„ฑ

์ด ์„ธ๊ฐ€์ง€์˜ permission์„ ์ž‘์„ฑํ• ๊ฒƒ์ด๋‹ค.

1. ๋ณธ์ธ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์ ‘๊ทผ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” IsOwner

2. ๋ณธ์ธ๋งŒ ์ˆ˜์ •,์‚ญ์ œํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋˜ ํ—ˆ์šฉ๋œ ํŒ”๋กœ์›Œ์—๊ฒŒ๋Š” ์กฐํšŒํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” IsOwnerOrReadOnly

3. ํŒ”๋กœ์ž‰ ์‹ ์ฒญ ์‹œ ๋ณธ์ธ๊ณผ ํŒ”๋กœ์ž‰ ์‹ ์ฒญ์— ๊ด€๋ จ๋œ ์‚ฌ๋žŒ๋งŒ ์กฐํšŒ,ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” IsFollowerOrOwner

IsOwner

๋ฐ์ดํ„ฐ์˜ ์†Œ์œ ์ž๋งŒ ์ ‘๊ทผํ•˜๊ณ  ์ˆ˜์ • ์‚ญ์ œ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค

class IsOwner(permissions.BasePermission):
    """
    ๋ณธ์ธ์˜ data๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋‹ค.
    """
    def has_permission(self, request, view):
        return request.user.is_authenticated

    def has_object_permission(self, request, view, obj):
        return obj.user == request.user

IsOwnerOrReadOnly

class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    ๊ฐ์ฒด๋ฅผ ๋งŒ๋“  ์‚ฌ์šฉ์ž๋งŒ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    """
    def has_permission(self, request, view):
        return request.user.is_authenticated

    def has_object_permission(self, request, view, obj):
        # ์š”์ฒญํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ํ•ด๋‹น ๊ฐ์ฒด์˜ ์†Œ์œ ์ž์ธ ๊ฒฝ์šฐ์—๋งŒ ์“ฐ๊ธฐ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•จ
        return obj.follower == request.user or obj.following_user == request.user

IsFollowerOrOwner

class IsFollowerOrOwner(permissions.BasePermission):
    """
    Custom permission to allow reading followed items only if they are open.
    """
    def has_permission(self, request, view):
        return request.user.is_authenticated

    def has_object_permission(self, request, view, obj):
        # Check if the request method is safe (GET, HEAD, OPTIONS)
        if request.method in permissions.SAFE_METHODS:
            if Follow.objects.filter(follower=request.user, following_user=obj.user, status='accepted').exists() | Follow.objects.filter(follower=obj.user, following_user=request.user, status='accepted').exists():
                return obj.is_open

        return obj.user == request.user

์œ„์˜ permissions๋“ค์„ ํ†ตํ•ด ์ด์ œ ์‚ฌ์šฉ์ž์— ๋”ฐ๋ฅธ ์ ‘๊ทผ ์ œ์–ด๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.

VIEW

ํŽธ๋ฆฌํ•œ API์˜ ๊ตฌ์„ฑ์„ ์œ„ํ•ด์„œ Mixins์™€ GenericViewSet์„ ์‚ฌ์šฉํ•œ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค.

class DiaryViewSet(GenericViewSet, # ๋ฏน์Šค์ธ ์‚ฌ์šฉ์„ ์œ„ํ•ด ๊ผญ ์ถ”๊ฐ€
                  mixins.ListModelMixin,#๋ฆฌ์ŠคํŠธ API
                  mixins.CreateModelMixin,#์ƒ์„ฑ API
                  mixins.RetrieveModelMixin,#์กฐํšŒ API
                  mixins.UpdateModelMixin,#์ˆ˜์ • API. ๋ถ€๋ถ„ ์ˆ˜์ •๊ณผ ์ „์ฒด ์ˆ˜์ • ์žˆ์Œ
                  mixins.DestroyModelMixin):#์‚ญ์ œ API

    # ์•„๋ž˜ ํผ๋ฏธ์…˜~์ฟผ๋ฆฌ์…‹์€ ํ•„์ˆ˜ ์ž‘์„ฑ
    permission_classes = [IsOwner]
    serializer_class = DiarySerializer
    queryset = Diary.objects.all()
    """
    ์—ฌ๊ธฐ์— ์ ๋Š” ์ฃผ์„์€ ํ›„์— swagger API๋ฌธ์„œ๋ฅผ ์œ„ํ•œ ๊ฒƒ. ์–ด๋–ค ๋ทฐ์ธ์ง€ ์ž‘์„ฑ.
    ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑ.
    ์ผ๊ธฐ์˜ ๋‚ด์šฉ์— ๋Œ€ํ•œ API
    """

    # ์ด ๋ถ€๋ถ„์€ ๋ฏน์Šค์ธ์„ ์‚ฌ์šฉํ•  ๋•Œ ์ฟผ๋ฆฌ๋ฅผ ํ•„ํ„ฐ๋งํ•˜์—ฌ ๋ณธ์ธ์˜ ๋ฐ์ดํ„ฐ๋งŒ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ์ฒ˜๋ฆฌํ•œ๊ฒƒ
    def filter_queryset(self,queryset):
        queryset = queryset.filter(user=self.request.user)
        return super().filter_queryset(queryset)

    # ์ด ์•„๋ž˜๋กœ ๋ถ€ํ„ฐ ์ถ”๊ฐ€๋กœ ์ปค์Šคํ…€์ด ํ•„์š”ํ•œ ๋ฏน์Šค์ธ๋“ค, ํ•จ์ˆ˜๋“ค์„ ์ž‘์„ฑํ•œ๋‹ค.
    # ์•„๋ž˜๋ถ€๋ถ„์—†์ด๋Š” ๊ธฐ๋ณธ ๋ฏน์Šค์ธ์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•œ๋‹ค

๊ธฐ๋ณธ ๋ฏน์Šค์ธ์˜ ํ•จ์ˆ˜๋“ค์€ ์•„๋ž˜ ๊นƒํ—™์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py

[django-rest-framework/rest_framework/mixins.py at master ยท encode/django-rest-framework

Web APIs for Django. ๐ŸŽธ. Contribute to encode/django-rest-framework development by creating an account on GitHub.

github.com](https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py)

์—ญ์‹œ ๋ทฐ๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ๋งŽ๊ธฐ ๋•Œ๋ฌธ์— ํŠน์ˆ˜ํ•œ ๊ฒฝ์šฐ๋งŒ ์‚ดํŽด๋ณด์ž.

Follow

ํŒ”๋กœ์ž‰ ๊ธฐ๋Šฅ์„ ๊ธฐ๋ณธ mixins๋“ค์˜ ๊ฐ ๊ธฐ๋Šฅ๋“ค์„ ํ™œ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€๋‹ค. (์•ฝ๊ฐ„ ์•ผ๋งค๋กœ ๊ตฌํ˜„ํ•œ ๋Š๋‚Œ.. ํ•˜์ง€๋งŒ ์ž˜ ๋Œ์•„๊ฐ„๋‹ค๋ฉด?)

1. permissions์˜ IsOwnerOrReadOnly๋ฅผ ํ†ตํ•ด ํŒ”๋กœ์ž‰์— ์—ฐ๊ด€๋œ ์‚ฌ์šฉ์ž๋งŒ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

2. filter_queryset์„ ํ†ตํ•ด ์—ฐ๊ด€๋œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

๊ฐœ๋ณ„ ํ•จ์ˆ˜๋ณ„ ์—ญํ• 

CREATE : ํŒ”๋กœ์šฐ ์š”์ฒญ

DESTROY: ํŒ”๋กœ์šฐ ์ทจ์†Œ/์‚ญ์ œ

UPDATE: ํŒ”๋กœ์šฐ ํ—ˆ์šฉ

PARTIAL_UPDATE: ํŒ”๋กœ์šฐ ๊ฑฐ์ ˆ

class FollowViewSet(GenericViewSet,
                           mixins.ListModelMixin,
                           mixins.CreateModelMixin,
                           mixins.DestroyModelMixin,
                           mixins.RetrieveModelMixin,
                           mixins.UpdateModelMixin):

    permission_classes = [IsOwnerOrReadOnly]
    serializer_class = FollowSerializer
    queryset = Follow.objects.all()
    def filter_queryset(self,queryset):
        queryset = queryset.filter(Q(follower=self.request.user) | Q(following_user=self.request.user))

        return super().filter_queryset(queryset)
    '''
    ํŒ”๋กœ์šฐ API

    ---

    ### id : ํŒ”๋กœ์šฐ ์š”์ฒญ์˜ id
    '''
    @swagger_auto_schema( request_body=openapi.Schema(
            type=openapi.TYPE_OBJECT,
            properties={
                'username': openapi.Schema(type=openapi.TYPE_STRING, description='ํŒ”๋กœ์šฐ ์š”์ฒญํ•  ์œ ์ €์˜ username')
            }
    ))
    def create(self, request, *args, **kwargs):
        '''
        ํŒ”๋กœ์šฐ ์š”์ฒญํ•˜๋Š” API

        '''
        # ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ๋ฐ›์Œ
        username = request.data.get('username')

        # ๋ฐ›์€ ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์Œ
        try:
            following_user = User.objects.get(username=username)
        except User.DoesNotExist:
            return Response({"message": f"User '{username}' does not exist"}, status=status.HTTP_404_NOT_FOUND)

        # ํŒ”๋กœ์šฐ ์š”์ฒญ ์ƒ์„ฑ์— ์‚ฌ์šฉํ•  ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ
        request_data = {
            'follower': request.user.id,
            'following_user': following_user.id,
            'status': Follow.REQUESTED
        }

        serializer = self.get_serializer(data=request_data)
        serializer.is_valid(raise_exception=True)

        user = self.request.user

        followee = serializer.validated_data.get('following_user')
        if followee==user:
            return Response({"message": f"Cannot Follow yourself, {followee.username}."}, status=status.HTTP_400_BAD_REQUEST)

        if Follow.objects.filter(follower=user, following_user=followee).exists() | Follow.objects.filter(follower=user, following_user=followee).exists():
            return Response({"message": f"Follow request already sent to {followee.username}."}, status=status.HTTP_400_BAD_REQUEST)


        follow_request, created = Follow.objects.get_or_create(follower=request.user, following_user=followee, status=Follow.REQUESTED)

        serializer = self.get_serializer(follow_request)


        if created:
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response({"message": f"Follow request already sent to {followee.username}"}, status=status.HTTP_400_BAD_REQUEST)


    def destroy(self, request, *args, **kwargs):
        '''
        ํŒ”๋กœ์šฐ ์š”์ฒญ ์‚ญ์ œ/์ทจ์†Œํ•˜๋Š” API
        '''

        instance = self.get_object()
        self.perform_destroy(instance)
        return Response({"message": "Follow request deleted"}, status=status.HTTP_204_NO_CONTENT)

    @swagger_auto_schema(
        request_body=openapi.Schema(
            type=openapi.TYPE_OBJECT,
            properties={}
    ))
    def update(self, request, *args, **kwargs):
        '''
        ํŒ”๋กœ์šฐ ์š”์ฒญ ํ—ˆ์šฉํ•˜๋Š” API

        '''

        instance = self.get_object()

        # ์š”์ฒญ๋ฐ›์€ ์‚ฌ์šฉ์ž๊ฐ€ ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์™€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ
        if instance.following_user != request.user:
            raise PermissionDenied("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค")


        instance.status = Follow.ACCEPTED
        instance.save()
        serializer = self.get_serializer(instance)
        return Response(serializer.data, status=status.HTTP_200_OK)


    def partial_update(self, request, *args, **kwargs):
        '''
        ํŒ”๋กœ์šฐ ์š”์ฒญ ๊ฑฐ์ ˆํ•˜๋Š” API

        '''
        instance = self.get_object()
        instance.status = Follow.REJECTED
        instance.save()
        serializer = self.get_serializer(instance)
        return Response(serializer.data, status=status.HTTP_200_OK)

AI ์ž‘์—…์ด ํ•„์š”ํ•œ Viewset๋“ค

์ด ์•„๋ž˜๋Š” API์š”์ฒญ์ด ๋“ค์–ด๊ฐ€๋Š” ์™ธ๋ถ€ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด AI์˜ ์ž‘์—…์ด ์žˆ๋Š” ๋ถ€๋ถ„์— ๋Œ€ํ•œ View์ด๋‹ค.

๋™์ž‘ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. ๋นจ๊ฐ„ ๊ธ€์”จ๊ฐ€ ์žˆ๋Š” ๋ถ€๋ถ„์ด AI์„œ๋ฒ„๋กœ ์š”์ฒญ์„ ๋„ฃ๋Š” ๋ถ€๋ถ„์ด๋‹ค.

Music

views ์ƒ๋‹จ์— AI ์„œ๋ฒ„๋กœ ์‘์•… ์ถ”์ฒœ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›๋Š” ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

def request_music_from_flask(content):
    """
    diary content ๋ฅผ ai์„œ๋ฒ„์— ์ „๋‹ฌ, ์Œ์•… ์ถ”์ฒœ ๋ฐ›์•„์˜ด
    """
    flask_url = f'http://{settings.FLASK_URL}:5000/get_music'
    try:
        response = requests.post(flask_url, json={'content': content},verify=False, timeout=50)
        if response.status_code == 200:
            response_data = response.json()
            time.sleep(2)
            return response_data
        else:
            print("Failed to get music from Flask:", response.status_code)
            return None
    except Exception as e:
        print("Error:", e)
        time.sleep(10)
        return None

์ด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์Œ์•… ๊ฐ์ฒด๋ฅผ ์ €์žฅํ•˜๊ณ , ์ผ๊ธฐ์— ์Œ์•… ๋ฐ์ดํ„ฐ๋ฅผ ์—ฐ๊ฒฐํ•œ๋‹ค.

    def update(self, request,*args, **kwargs):
        """
        diary_music_update ์ผ๊ธฐ์— ๋Œ€ํ•ด ์Œ์•…์„ ์ถ”์ฒœํ•˜๋Š” API

        ---
        ### id = ์ผ๊ธฐ ID
        ์ตœ๋Œ€ 15์ดˆ ์†Œ์š” ๊ฐ€๋Šฅ
        ### ์˜ˆ์‹œ request:

                {
                    "user": 1,
                }

        ### ์˜ˆ์‹œ response:
                200
                {
                    "id": 1,
                    "user": 1,
                    "content": "๋„ˆ๋ฌด ๋‘๊ทผ๊ฑฐ๋ฆฐ๋‹ค! ๊ณผ์—ฐ rds์— ๋‚ด ๋‹ค์ด์–ด๋ฆฌ๊ฐ€ ์ž˜ ์˜ฌ๋ผ๊ฐˆ๊นŒ? ์˜ค๋Š˜ ์ด๊ฒƒ๋งŒ ์„ฑ๊ณตํ•˜๋ฉด ๋„ˆ๋ฌด ์ฆ๊ฑฐ์šด ๋งˆ์Œ์œผ๋กœ ์ž˜ ์ˆ˜ ์žˆ์„๊ฒƒ ๊ฐ™๋‹ค!",
                    "music": {
                        "id": 1,
                        "music_title": "๊ทธ๋Œ€๋งŒ ์žˆ๋‹ค๋ฉด (์—ฌ๋ฆ„๋‚  ์šฐ๋ฆฌ X ๋„ˆ๋“œ์ปค๋„ฅ์…˜ (Nerd Connection))",
                        "artist": "๋„ˆ๋“œ์ปค๋„ฅ์…˜ (Nerd Connection)",
                        "genre": "๋ฐœ๋ผ๋“œ"
                    }
                }
                401 
                400
                {'detail': 'Failed to get similar music from Flask'}

        """
        partial = kwargs.pop('partial', True)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)

        print(serializer.data['content'])
        response = request_music_from_flask(serializer.data['content'])
        best_music = response.get('most_similar_song')
        print(best_music)
        similar_songs = response.get('similar_songs')
        print(similar_songs)
        if best_music:
            music, created = Music.objects.get_or_create(music_title=best_music['title'], artist=best_music['artist'], genre=best_music['genre'])
            instance.music = music

            serializer = self.get_serializer(instance, data=request.data, partial=partial)
            serializer.is_valid(raise_exception=True)
            self.perform_update(serializer)

            return Response(serializer.data, status=status.HTTP_200_OK)
        else:
            return Response({'detail': 'Failed to get similar music from Flask'}, status=status.HTTP_400_BAD_REQUEST)

Emotion&Chat

views ์ƒ๋‹จ์— AI ์„œ๋ฒ„๋กœ emotion label ๊ฒ€์ถœ๊ณผ ์‘์› ๋ฌธ๊ตฌ(comment) ๋ฅผ ์ƒ์„ฑํ•˜๋„๋ก ์š”์ฒญํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

#views.py์˜ ์ƒ๋‹จ์— AI์„œ๋ฒ„๋กœ ์š”์ฒญ์„ํ•˜๊ณ  ์‘๋‹ต์„ ๋ฐ›๋Š” ํ•จ์ˆ˜ ์ถ”๊ฐ€

def request_emotion(content):
    """
    ์ผ๊ธฐ ๋‚ด์šฉ์œผ๋กœ emootion label ๊ฒ€์ถœ
    """
    flask_url = f'http://{settings.FLASK_URL}:5000/get_sentiment'
    try:

        response = requests.post(flask_url, json={'content': content},verify=False, timeout=50)

        if response.status_code == 200:
            response_data = response.json()
            emotion_label = response_data['emotion_label']
            print("Received emotion_label:", emotion_label)
            time.sleep(2)

            return emotion_label

        else:
            print("Failed to get emotion from Flask:", response.status_code)

            return None

    except Exception as e:
        print("Error:", e)
        time.sleep(10)

        return None

def request_comment(content):
    """
    ์ผ๊ธฐ ๋‚ด์šฉ์œผ๋กœ ์‘์› ๋ฌธ๊ตฌ ์ƒ์„ฑ
    """
    flask_url = f'http://{settings.FLASK_URL}:5000/get_comment'
    try:

        response = requests.post(flask_url, json={'content': content},verify=False, timeout=50)

        if response.status_code == 200:
            response_data = response.json()
            comment = response_data['comment']
            print("Received comment:", comment)
            time.sleep(2)

            return comment

        else:
            print("Failed to get comment from Flask:", response.status_code)

            return None

    except Exception as e:
        print("Error:", e)
        time.sleep(10)

        return None

์ด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ViewSet๋‚ด๋ถ€์— ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

#ViewSet๋‚ด๋ถ€์— create ์ž‘์„ฑ

    def create(self, request, *args, **kwargs):
        """
        emotion_create ์ผ๊ธฐ ๋‚ด์šฉ์œผ๋กœ ๊ฐ์ •๋ผ๋ฒจ, ์‘์›๋ฌธ๊ตฌ ์ƒ์„ฑ ํ•˜๋Š” API

        ---
        ## ์˜ˆ์‹œ request:

            {
                'diary' : 2
            }

        ## ์˜ˆ์‹œ response:

            200
            {
                "id": 2,
                "emotion_label": "๋ถˆ์•ˆ",
                "emotion_prompt": "",
                "chat": " ์ด๋ณ„์€ ์‚ฌ์‹ค์ผ์ง€๋„ ๋ชจ๋ฅด๊ฒ ์–ด์š” ",
                "diary": 2
            }

        """
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        diary = serializer.validated_data.get('diary')

        if diary.user != request.user:
            return Response({'error': "Diary does not belong to the current user."}, status=status.HTTP_400_BAD_REQUEST)

        chat = request_comment(diary.content)
        label = request_emotion(diary.content)

        existing_emotion = Emotion.objects.filter(diary=diary).first()
        if existing_emotion:
            serializer = self.get_serializer(existing_emotion, data={'chat': chat, 'emotion_label': label}, partial=True)
            serializer.is_valid(raise_exception=True)
            serializer.save(diary=diary, chat=chat, emotion_label = label)

        else:
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            serializer.save(diary=diary, chat=chat, emotion_label = label)

        return Response(serializer.data, status=status.HTTP_201_CREATED)

Image

GPT - ์ด๋ฏธ์ง€ ์ƒ์„ฑ์šฉ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ

GPT API์™€ ๋ฏธ๋ฆฌ ์ •์˜ํ•œ GPT ํ”„๋กฌํ”„ํŠธ๋ฅผ ํ•ฉํ•˜์—ฌ GPT์— ์ด๋ฏธ์ง€ ์ƒ์„ฑ์šฉ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

from django.conf import settings 
import openai

with open(f"{settings.BASE_DIR}/ai/genTextBase.txt", 'r', encoding='utf-8') as file:
    base_text = ''.join(file.readlines())

    api_key = settings.OPENAI_API_KEY
    openai.api_key=api_key

def get_prompt(content):
    """
    ์ผ๊ธฐ ๋‚ด์šฉ์„ ์ž…๋ ฅ๋ฐ›์•„ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
    """
    completion = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": f"{base_text} {content.strip()}"},]
    )
    generated_text = completion["choices"][0]["message"]["content"]
    output_text=generated_text.split('\n')
    pre_text = "(masterpiece,detailed), (Oil Painting:1.3), (Impressionism:1.3) ,(oil painting with brush strokes:1.2), (looking away:1.1), "
    prompts = [pre_text+v for v in output_text if v]
    return prompts

Flask&AI์„œ๋ฒ„ - ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ

์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ „๋‹ฌํ•ด AI์„œ๋ฒ„์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฏธ์ง€ url์„ ์‘๋‹ต์œผ๋กœ ๋ฐ›๋Š”๋‹ค.

def request_image_from_flask(prompt):
    """
    ์ƒ์„ฑ๋œ prompt๋กœ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
    """
    flask_url = f'http://{settings.FLASK_URL}:5000/get_image'

    try:
        # HTTP POST ์š”์ฒญ์œผ๋กœ prompt๋ฅผ Flask์— ์ „์†ก
        response = requests.post(flask_url, json={'prompt': prompt},verify=False, timeout=150)
        # ์‘๋‹ต ํ™•์ธ
        if response.status_code == 200:
            # ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„ฑ๊ณต
            response_data = response.json()
            image_url = response_data['image_url']
            print("Received image url:", image_url)
            time.sleep(2)
            return image_url
        else:
            # ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ
            print("Failed to get image from Flask:", response.status_code)
            return None
    except Exception as e:
        print("Error:", e)
        time.sleep(10)
        return None

Viewset - createํ•จ์ˆ˜ ์ž‘์„ฑ

์œ„์—์„œ ์ž‘์„ฑํ•œ ํ•จ์ˆ˜๋“ค์„ ํ†ตํ•ด ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ „๋‹ฌ๋ฐ›์€ url์„ ํ†ตํ•ด ์ด๋ฏธ์ง€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ € ์ƒ์„ฑ

    def create(self, request, *args, **kwargs):

        '''
        ์ด๋ฏธ์ง€ ์ƒ์„ฑ API

        ---

        ### ์‘๋‹ต์— ์ตœ๋Œ€ 40์ดˆ ์†Œ์š” ๊ฐ€๋Šฅ 
        ## ์˜ˆ์‹œ request:

            {
                'diary' : 1
            }

        ## ์˜ˆ์‹œ response:

            201
            {
                  "id": 70,
                    "created_at": "2024-05-02T13:04:10.208658+09:00",
                    "image_url": "https://๋ฒ„ํ‚ท์ฃผ์†Œ/images/826cb58e-46a3-41fc-9699-bc2eccdc1355.jpg",
                    "image_prompt": "(masterpiece,detailed), (Oil Painting:1.3), (Impressionism:1.3) ,(oil painting with brush strokes:1.2), (looking away:1.1), a girl in a traditional Korean hanbok, cherry blossom background, soft pastel colors, Korean artist reference, (ethereal:1.2), (delicate details:1.3), (dreamy atmosphere:1.25)",
                    "diary": 1
            }
            400
            {
                'error': "Failed to get image from Flask" ์ด ๊ฒฝ์šฐ AI ์„œ๋ฒ„๊ฐ€ ๊บผ์ ธ์žˆ์„๋•Œ์ž„
            }
            400
            {
                'error': "Error uploading image: {str(e)}"
            }
            401 
            403 
        '''

        try:
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)

            diary = serializer.validated_data.get('diary')
            image_prompt = get_prompt(diary.content)[0]
            image_url = request_image_from_flask(image_prompt)

            if not image_url:
                return Response({'error': "Failed to get image from Flask"}, status=status.HTTP_400_BAD_REQUEST)

            new_image = Image.objects.get_or_create(diary=diary, image_url=image_url, image_prompt=image_prompt)
            serializer.validated_data['diary'] = diary
            serializer.validated_data['image_url'] = image_url
            serializer.validated_data['image_prompt'] = image_prompt
            serializer.save()

            return Response(serializer.data, status=status.HTTP_201_CREATED)

        except Exception as e:
                return Response({'error': f"Error uploading image: {str(e)}"}, status=status.HTTP_400_BAD_REQUEST)

์ด๊ฒƒ์œผ๋กœ ๋ฐฑ์—”๋“œ Django API๋ถ€๋ถ„์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์€ ์ด๊ฒƒ์œผ๋กœ ๊ตฌํ˜„์ด ๋๋‚œ๋‹ค.

API๋ฌธ์„œํ™”

API ๊ตฌํ˜„์ด ๋๋‚ฌ๋‹ค๋ฉด ํ”„๋ก ํŠธ๋กœ ๊ฐ API์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋„˜๊ฒจ์ค˜์•ผ ์ž‘์—…์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์ด๊ฒƒ์„ ์šฐ๋ฆฌ๋Š” Swagger๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•œ๋‹ค. ์žฅ๊ณ  REST Framework์—์„œ swagger๋ฅผ ํ†ตํ•ด API๋ฅผ ๋ฌธ์„œํ™” ํ•ด๋ณด์ž.

๋จผ์ € drf-ysag์— ๋Œ€ํ•œ ๋ฌธ์„œ๋Š” ์•„๋ž˜์˜ ๋งํฌ๋ฅผ ํ†ตํ•ด ์ด๋™ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ƒ์„ธํ•œ ์ •๋ณด๋ฅผ ์›ํ•œ๋‹ค๋ฉด ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ™œ์šฉํ•˜์ž.

https://drf-yasg.readthedocs.io/en/stable/

install

โ€‹
๊ฐ€์ƒํ™˜๊ฒฝ์„ ํ™œ์„ฑํ™” ํ•˜๊ณ  install ํ•˜์ž.
โ€‹

pip install -U drf-yasg

โ€‹

settings.py

โ€‹
settings.py์— ๋‹ค์Œ์ด ์ถ”๊ฐ€๋˜์–ด์•ผ ํ•œ๋‹ค.
โ€‹

INSTALLED_APPS = [
   ...
   'django.contrib.staticfiles',  # required for serving swagger ui's css/js files
   'drf_yasg',
   ...
]

โ€‹

urls.py

โ€‹
์ด 4๊ฐ€์ง€์˜ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ• ๊ฒƒ์ด๋‹ค.

  • A JSON view of your API specification at /swagger.json

  • A YAML view of your API specification at /swagger.yaml

  • A swagger-ui view of your API specification at /swagger/

  • A ReDoc view of your API specification at /redoc/
    โ€‹
    ํ”„๋กœ์ ํŠธ์˜ urls.py์— ๋‹ค์Œ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.
    โ€‹

    ...
    from django.urls import re_path
    from rest_framework import permissions
    from drf_yasg.views import get_schema_view
    from drf_yasg import openapi
    โ€‹
    ...
    โ€‹
    schema_view = get_schema_view(
    openapi.Info(
      title="Snippets API",
      default_version='v1',
      description="Test description",
      terms_of_service="https://www.google.com/policies/terms/",
      contact=openapi.Contact(email="contact@snippets.local"),
      license=openapi.License(name="BSD License"),
    ),
    public=True,
    permission_classes=(permissions.AllowAny,),
    )
    โ€‹
    urlpatterns = [
    path('swagger<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
    path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
    path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
    ...
    ]

    โ€‹ํ™•์ธโ€‹
    runserver ํ•ด์ฃผ๊ณ , /swagger/์— ์ ‘๊ทผํ•œ๋‹ค.
    โ€‹
    ๊ทธ๋Ÿผ ๋‹ค์Œ์ฒ˜๋Ÿผ api๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
    โ€‹

Flask : AI๋ชจ๋ธ ์„œ๋น™

์ด๋ฒˆ์—๋Š” GPU์„œ๋ฒ„์— ์˜ฌ๋ฆด Flask ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

Flask์—์„œ๋Š” ๋ชจ๋ธ์„ ๋กœ๋“œํ•˜๊ณ , ๋ฐฑ์—”๋“œ API์— ํ•„์š”ํ•œ AI์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

GPU๋น„์šฉ ๋ฌธ์ œ๋กœ, GPU๊ฐ€ ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋งŒ ์ด ์„œ๋ฒ„์—์„œ ๋™์ž‘ํ•œ๋‹ค.

์†Œ๊ฐœ

Flask์—์„œ๋Š” ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
โ€‹
1. generate comment : ์ผ๊ธฐ ์ž‘์„ฑ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์‘์› ๋ฌธ๊ตฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

2. generate image : ๋ฐฑ์—”๋“œ๋กœ๋ถ€ํ„ฐ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฐ›๊ณ , ํŠœ๋‹๋œ ๋””ํ“จ์ „ ๋ชจ๋ธ์„ ๋กœ๋“œํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

3. emotion classification : ์ผ๊ธฐ ์ž‘์„ฑ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ์ • ๋ถ„์„ํ•œ๋‹ค.

4. recommend music : ๊ฐ์ • ๋ถ„์„ ๊ฒฐ๊ณผ์™€ ํฌ๋กค๋ง์„ ํ†ตํ•ด ์ˆ˜์ง‘ํ•œ ์Œ์•… ๋ฐ์ดํ„ฐ์˜ ๊ฐ์ •๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ•ด ์œ ์‚ฌ๋„๋ฅผ ํ†ตํ•œ ์Œ์•… ์ถ”์ฒœ, ๊ฒฐ๊ณผ ์ด 5๊ฐ€์ง€ ๋ฐ˜ํ™˜
โ€‹

Image

โ€‹
์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฐ›์•„์„œ ์ด๋ฏธ์ง€ ์ƒ์„ฑ, S3๋ฒ„ํ‚ท์— ์ €์žฅํ•˜๊ณ , URL์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋จผ์ € ๋‹ค๋ฅธ ํŒŒ์ผ์— ์ž‘์„ฑํ•˜์ž.
โ€‹

#generate_image.py ์— ๋ชจ๋ธ์„ ๋กœ๋“œํ•˜๊ณ , ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.
from diffusers import StableDiffusionPipeline
import torch
โ€‹
# ๋ชจ๋ธ ๋กœ๋“œ ๋ฐ ๋””๋ฐ”์ด์Šค ์„ค์ •
model_path = '๋ชจ๋ธ ์œ„์น˜'  # FineTuning Model Path
pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5")
device = torch.device('cuda')
pipe.unet.load_attn_procs(model_path)
pipe.to(device)
โ€‹
# Negative prompt ์„ค์ •
neg_prompt = '''FastNegativeV2,(bad-artist:1.0), (loli:1.2),
    (worst quality, low quality:1.4), (bad_prompt_version2:0.8),
    bad-hands-5,lowres, bad anatomy, bad hands, ((text)), (watermark),
    error, missing fingers, extra digit, fewer digits, cropped,
    worst quality, low quality, normal quality, ((username)), blurry,
    (extra limbs), bad-artist-anime, badhandv4, EasyNegative,
    ng_deepnegative_v1_75t, verybadimagenegative_v1.3, BadDream,
    (three hands:1.1),(three legs:1.1),(more than two hands:1.4),
    (more than two legs,:1.2),badhandv4,EasyNegative,ng_deepnegative_v1_75t,verybadimagenegative_v1.3,(worst quality, low quality:1.4),text,words,logo,watermark,
    '''
โ€‹
def get_image(prompt):
    image = pipe(prompt, negative_prompt=neg_prompt,num_inference_steps=30, guidance_scale=7.5).images[0]
    return image

โ€‹
๋Œ€์ฒด๋กœ 15์ดˆ์ •๋„ ์†Œ์š”๋˜๊ณ , ๋Šฆ์–ด์ง€๋ฉด 30์ดˆ ์ด๋‚ด๋กœ ์ƒ์„ฑ๋œ๋‹ค. ํ•˜์ง€๋งŒ ์‚ฌ์‹ค ๊ฝค ๊ธด ์‹œ๊ฐ„์ด๋ฏ€๋กœ ์ด๋ฅผ ์ž˜ ๊ณ ๋ คํ•ด์„œ ์‘๋‹ต์„ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐฑ์—”๋“œ ์„œ๋ฒ„์—์„œ ์‘๋‹ต ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ 40์ดˆ ์ •๋„๋กœ ๋Š˜๋ ค์ฃผ์ž.(์œ„์˜ ์ฝ”๋“œ์— ์‚ฌ์‹ค ์ด๋ฏธ ์ ์šฉ๋˜์–ด์žˆ๋‹ค)
โ€‹

#app.py
โ€‹
# S3์— ์—ฐ๊ฒฐ
s3 = boto3.client('s3',
                  aws_access_key_id=S3_ACESS_KEY,
                  aws_secret_access_key=S3_SECRET_ACCESS_KEY)
โ€‹
# ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ์— ๋Œ€ํ•œ ์ž‘์—…
@app.route('/get_image', methods=['POST'])
async def process_image_request():
    try:
        # ๋ฐ›์€ ์š”์ฒญ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ธ
        request_data = request.json
        prompt = request_data.get('prompt')

        #๋ฐ›์€ ํ”„๋กฌํ”„ํŠธ๋กœ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
        image = get_image(prompt)

        # ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•œ๋‹ค.
        image_key = str(uuid.uuid4())
        buffered = io.BytesIO()
        image.save(buffered, format="JPEG")
        buffered.seek(0)
        s3.upload_fileobj(buffered, Bucket=S3_BUCKET_NAME, Key=f'images/{image_key}.jpg', ExtraArgs={'ContentType':'image/jpeg'})
        image_url = f'https://{S3_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/images/{image_key}.jpg'
        buffered.close()

        # ์ €์žฅ ํ›„ ์ด๋ฏธ์ง€์˜ URL์„ ์‘๋‹ตํ•œ๋‹ค.
        return jsonify({'image_url': image_url}), 200
    except Exception as e:
        print("Exception occurred in process_request:", e)
        return jsonify({"error": str(e)}), 500

โ€‹

Comment

โ€‹
ํ”„๋กœ์„ธ์Šค๋Š” ์œ„์™€ ๊ฐ™๋‹ค. ๋ชจ๋ธ์„ ๋กœ๋“œํ•˜์—ฌ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค๊ณ , API์š”์ฒญ์— ๋Œ€ํ•œ ์ž‘์—…์„ app.py์— ์ž‘์„ฑํ•œ๋‹ค.
โ€‹

#๋ชจ๋ธ ๋กœ๋“œ
#gpt model
print('gpt_load')
gpt_device = torch.device("cuda:0")
gpt_model = GPT2LMHeadModel.from_pretrained('๋ชจ๋ธ').to(gpt_device)
gpt_tokenizer = PreTrainedTokenizerFast.from_pretrained('๋ชจ๋ธ')
U_TKN = '<usr>'
S_TKN = '<sys>'
BOS = '</s>'
EOS = '</s>'
MASK = '<unused0>'
SENT = '<unused1>'
PAD = '<pad>'
โ€‹
def get_comment(input_text): #koGPT2 ๋ชจ๋ธ์„ ํ™œ์šฉํ•˜์—ฌ ์ž…๋ ฅ๋œ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋Œ€๋‹ต์„ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜
    q = input_text
    a = ""
    sent = ""
    while True:
        input_ids = torch.LongTensor(gpt_tokenizer.encode(U_TKN + q + SENT + sent + S_TKN + a)).unsqueeze(dim=0).to(gpt_device)
        pred = gpt_model(input_ids)
        pred = pred.logits
        gen = gpt_tokenizer.convert_ids_to_tokens(torch.argmax(pred, dim=-1).squeeze().tolist())[-1]
        if gen == EOS:
            break
        a += gen.replace("โ–", " ")
    return a

โ€‹

# app.py
@app.route('/get_comment', methods=['POST'])
async def process_comment_request():
    try:
        request_data = request.json
        content = request_data.get('content')
        comment = get_comment(content)

        return jsonify({'comment': comment}), 200
    except Exception as e:
        print("Exception occurred in process_request:", e)
        return jsonify({"error": str(e)}), 500

โ€‹

Emotion & Music

โ€‹

def get_emotion_label(content):
    emotion_pred = inference(content)
    max_value = max(emotion_pred)
    max_index = emotion_pred .index(max_value)
    return emotion_pred, emotion_arr[max_index]
โ€‹
โ€‹
def get_music(content):
    emotion_pred, max_index=get_emotion_label(content)
    df_user_sentiment = pd.DataFrame([emotion_pred],columns=emotion_arr)
    user_emotion_str = df_user_sentiment.apply(lambda x: ' '.join(map(str, x)), axis=1)
    music_emotion_str = final_emotion[emotion_arr].apply(lambda x: ' '.join(map(str, x)), axis=1)
โ€‹
    tfidf = TfidfVectorizer()
    user_tfidf_matrix = tfidf.fit_transform(user_emotion_str)
    music_tfidf_matrix = tfidf.transform(music_emotion_str)
โ€‹
    cosine_sim = cosine_similarity(user_tfidf_matrix, music_tfidf_matrix)

    most_similar_song_index = cosine_sim.argmax()
    most_similar_song_info = final_emotion.iloc[most_similar_song_index]
โ€‹
    num_additional_recommendations = 4
    similar_songs_indices = cosine_sim.argsort()[0][-num_additional_recommendations-1:-1][::-1]
    similar_songs_info = final_emotion.iloc[similar_songs_indices]
โ€‹
    return most_similar_song_info, similar_songs_info

โ€‹

@app.route('/get_sentiment', methods=['POST'])
async def process_sentiment_request():
    try:
        request_data = request.json
        content = request_data.get('content')
        _, emotion_label = get_emotion_label(content)

        return jsonify({'emotion_label': emotion_label}), 200
    except Exception as e:
        print("Exception occurred in process_request:", e)
        return jsonify({"error": str(e)}), 500
โ€‹
@app.route('/get_music', methods=['POST'])
async def process_music_request():
    try:
        request_data = request.json
        content = request_data.get('content')
        most_similar_song_info, similar_songs_info = get_music(content)

        response_data = {
            'most_similar_song': {
                'title': most_similar_song_info[0],
                'artist': most_similar_song_info[1],
                'genre': most_similar_song_info[2]
            },
            'similar_songs': [{
                'title': song_info[0],
                'artist': song_info[1],
                'genre': song_info[2]
            } for song_info in similar_songs_info.values]
        }
        return jsonify(response_data), 200
    except Exception as e:
        print("Exception occurred in process_request:", e)
        return jsonify({"error": str(e)}), 500

GCP-VM์— ์˜ฌ๋ฆฌ๊ธฐ

์ž‘์„ฑํ•œ Flask๋Š” Github์— ์˜ฌ๋ฆฌ๊ณ , VM์— ์„œ๋น™ํ•˜๋„๋ก ํ•œ๋‹ค.