์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ Python ์›น ์•ฑ์œผ๋กœ ๋งŒ๋“œ๋Š” ๋‚˜๋งŒ์˜ ์ดˆ๊ฐ„๋‹จ ์˜จ๋ผ์ธ ์„ค๋ฌธ ์กฐ์‚ฌ ์‹œ์Šคํ…œ: ์ฝ”๋“œ ํ•œ ์ค„๋ถ€ํ„ฐ ๋ฐฐํฌ๊นŒ์ง€! ๐Ÿš€

๐Ÿ Python ์›น ์•ฑ์œผ๋กœ ๋งŒ๋“œ๋Š” ๋‚˜๋งŒ์˜ ์ดˆ๊ฐ„๋‹จ ์˜จ๋ผ์ธ ์„ค๋ฌธ ์กฐ์‚ฌ ์‹œ์Šคํ…œ: ์ฝ”๋“œ ํ•œ ์ค„๋ถ€ํ„ฐ ๋ฐฐํฌ๊นŒ์ง€! ๐Ÿš€

ํŒŒ์ด์ฌ์˜ ๋งˆ๋ฒ•์œผ๋กœ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์„ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๋Š” ์™„์ „์ •๋ณต ๊ฐ€์ด๋“œ

์•ˆ๋…•ํ•˜์„ธ์š” ์—ฌ๋Ÿฌ๋ถ„! ์˜ค๋Š˜์€ ์ง„์งœ ๊ฟ€์žผ ์ฃผ์ œ๋กœ ์ฐพ์•„์™”์–ด์š”~ ๐Ÿฏ Python์œผ๋กœ ๋‚˜๋งŒ์˜ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์„ ๋š๋”ฑ ๋งŒ๋“ค์–ด๋ณด๋Š” ์‹œ๊ฐ„์„ ๊ฐ€์ ธ๋ณผ๊ฒŒ์š”! ์š”์ฆ˜ ์„ค๋ฌธ์กฐ์‚ฌ ๋งŽ์ด ํ•˜์‹œ์ฃ ? ํ•™๊ต ๊ณผ์ œ, ํšŒ์‚ฌ ํ”„๋กœ์ ํŠธ, ์‹ฌ์ง€์–ด ์นœ๊ตฌ๋“ค ์ทจํ–ฅ ํŒŒ์•…ํ•  ๋•Œ๋„ ์„ค๋ฌธ์กฐ์‚ฌ ํ•œ ๋ฒˆ์”ฉ ๋Œ๋ฆฌ๊ฒŒ ๋˜๋Š”๋ฐ... ๊ธฐ์กด ์„œ๋น„์Šค ์“ฐ๊ธฐ ์ง€๊ฒน๋‹ค๋ฉด? ๋‚ด ๋ง˜๋Œ€๋กœ ์ปค์Šคํ…€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด? ์ง์ ‘ ๋งŒ๋“ค์–ด๋ฒ„๋ฆฌ๋Š” ๊ฑฐ ์–ด๋–จ๊นŒ์š”? ใ…‹ใ…‹ใ…‹

์ด ๊ธ€์—์„œ๋Š” ํŒŒ์ด์ฌ ์ดˆ๋ณด์ž๋„ ๋”ฐ๋ผํ•  ์ˆ˜ ์žˆ๊ฒŒ Flask ์›น ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ํ™œ์šฉํ•ด ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋งŒ๋“ค์–ด๋ณด๋Š” ๊ณผ์ •์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”! ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™๋ถ€ํ„ฐ ์‹ค์‹œ๊ฐ„ ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”๊นŒ์ง€, ์ง„์งœ ์‹ค๋ฌด์—์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์œผ๋กœ์š”! ๐Ÿ˜Ž

์ฐธ๊ณ ๋กœ ์ด๋Ÿฐ ๊ฐœ๋ฐœ ์Šคํ‚ฌ์€ ์š”์ฆ˜ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ธ๊ธฐ ๋งŒ์ ์ด๋ผ๊ณ  ํ•ด์š”. ์‹ค์ œ๋กœ ์žฌ๋Šฅ๋„ท์—์„œ Python ์›น ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์ด ๋งŽ์ด ๊ฑฐ๋ž˜๋œ๋‹ค๊ณ  ํ•˜๋‹ˆ, ์ด ๊ธ€์„ ํ†ตํ•ด ๋ฐฐ์šด ๋‚ด์šฉ์œผ๋กœ ๋ถ€์—…๊นŒ์ง€ ๋…ธ๋ ค๋ณผ ์ˆ˜ ์žˆ๊ฒ ์ฃ ? ์ผ์„์ด์กฐ! ๐Ÿ‘

๐Ÿ“š ๋ชฉ์ฐจ

  1. ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์˜ ์ดํ•ด์™€ ๊ธฐํšํ•˜๊ธฐ
  2. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •ํ•˜๊ธฐ
  3. Flask๋กœ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ผˆ๋Œ€ ๋งŒ๋“ค๊ธฐ
  4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์—ฐ๋™ํ•˜๊ธฐ
  5. ์„ค๋ฌธ ์ƒ์„ฑ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ
  6. ์‘๋‹ต ์ˆ˜์ง‘ ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ
  7. ๊ฒฐ๊ณผ ๋ถ„์„ ๋ฐ ์‹œ๊ฐํ™” ๊ตฌํ˜„ํ•˜๊ธฐ
  8. ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ ์ถ”๊ฐ€ํ•˜๊ธฐ
  9. ๋ฐฐํฌ ๋ฐ ์šด์˜ํ•˜๊ธฐ
  10. ํ™•์žฅ ๋ฐ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์•„์ด๋””์–ด

1. ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์˜ ์ดํ•ด์™€ ๊ธฐํšํ•˜๊ธฐ ๐ŸŽฏ

์ผ๋‹จ ๋ญ๋“  ๋งŒ๋“ค๊ธฐ ์ „์— "์ด๊ฒŒ ๋ญ”๋ฐ?" ํ•˜๋Š” ์ดํ•ด๊ฐ€ ํ•„์š”ํ•˜๊ฒ ์ฃ ? ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์ด ์–ด๋–ค ๊ธฐ๋Šฅ์„ ํ•ด์•ผ ํ•˜๋Š”์ง€ ํ™•์‹คํžˆ ์•Œ๊ณ  ์‹œ์ž‘ํ•ด๋ด…์‹œ๋‹ค!

1.1 ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ

์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์€ ํฌ๊ฒŒ ์„ธ ํŒŒํŠธ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์–ด์š”:

  1. ์„ค๋ฌธ ์ œ์ž‘ ํŒŒํŠธ: ์งˆ๋ฌธ ๋งŒ๋“ค๊ณ , ์˜ต์…˜ ์„ค์ •ํ•˜๊ณ , ์„ค๋ฌธ์ง€ ๋””์ž์ธํ•˜๋Š” ๋ถ€๋ถ„
  2. ์‘๋‹ต ์ˆ˜์ง‘ ํŒŒํŠธ: ์‚ฌ์šฉ์ž๋“ค์ด ์„ค๋ฌธ์— ๋‹ต๋ณ€ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค
  3. ๊ฒฐ๊ณผ ๋ถ„์„ ํŒŒํŠธ: ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” ๋ถ€๋ถ„

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ์‹œ์Šคํ…œ์€ ์ด ์„ธ ๊ฐ€์ง€๋ฅผ ๋‹ค ํฌํ•จํ•  ๊ฑฐ์˜ˆ์š”. ์ง„์งœ ๊ตฌ๊ธ€ ํผ์Šค ๋บจ์น˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณผ ๊ฑฐ๋‹ˆ๊นŒ ๊ธฐ๋Œ€ํ•˜์…”๋„ ์ข‹์•„์š”! ใ…‹ใ…‹ใ…‹

1.2 ์‹œ์Šคํ…œ ๊ธฐํšํ•˜๊ธฐ

๊ฐœ๋ฐœ ์‹œ์ž‘ ์ „์— ๊ฐ„๋‹จํ•œ ๊ธฐํš์„ ํ•ด๋ณผ๊ฒŒ์š”. ์šฐ๋ฆฌ ์‹œ์Šคํ…œ์€ ์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์„ ๊ฐ€์งˆ ๊ฑฐ์˜ˆ์š”:

  1. ๋‹ค์–‘ํ•œ ์งˆ๋ฌธ ์œ ํ˜• ์ง€์› (๊ฐ๊ด€์‹, ์ฃผ๊ด€์‹, ์ฒดํฌ๋ฐ•์Šค ๋“ฑ)
  2. ์„ค๋ฌธ ๊ฒฐ๊ณผ ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ
  3. ๋ชจ๋ฐ”์ผ ์นœํ™”์  ๋””์ž์ธ
  4. ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ
  5. ์„ค๋ฌธ ๊ณต์œ  ๊ธฐ๋Šฅ
์„ค๋ฌธ ์ œ์ž‘ ์งˆ๋ฌธ ์ถ”๊ฐ€ ์˜ต์…˜ ์„ค์ • ๋””์ž์ธ ๋ณ€๊ฒฝ ์„ค๋ฌธ ์ €์žฅ ์‘๋‹ต ์ˆ˜์ง‘ ๋งํฌ ๊ณต์œ  ์‘๋‹ต ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ์ €์žฅ ์‘๋‹ต ํ™•์ธ ๊ฒฐ๊ณผ ๋ถ„์„ ํ†ต๊ณ„ ์ฒ˜๋ฆฌ ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ ๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ ์ธ์‚ฌ์ดํŠธ ๋„์ถœ Python ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ ๊ตฌ์กฐ

์œ„ ๋‹ค์ด์–ด๊ทธ๋žจ์„ ๋ณด๋ฉด ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ์‹œ์Šคํ…œ์˜ ์ „์ฒด ๊ตฌ์กฐ๊ฐ€ ํ•œ๋ˆˆ์— ๋“ค์–ด์˜ค์ฃ ? ์ด์ œ ์ด๊ฑธ ์‹ค์ œ๋กœ ๊ตฌํ˜„ํ•ด๋ณผ ์ฐจ๋ก€์˜ˆ์š”! ๐Ÿค“

1.3 ๊ธฐ์ˆ  ์Šคํƒ ์„ ํƒํ•˜๊ธฐ

์šฐ๋ฆฌ๋Š” ์ด๋Ÿฐ ๊ธฐ์ˆ ๋“ค์„ ์‚ฌ์šฉํ•  ๊ฑฐ์˜ˆ์š”:

  1. ๋ฐฑ์—”๋“œ: Python + Flask (๊ฐ€๋ณ๊ณ  ๋ฐฐ์šฐ๊ธฐ ์‰ฌ์›Œ์„œ ๊ตฟ!)
  2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: SQLite (๊ฐœ๋ฐœ์šฉ) / PostgreSQL (์‹ค์ œ ๋ฐฐํฌ์šฉ)
  3. ํ”„๋ก ํŠธ์—”๋“œ: HTML, CSS, JavaScript + Chart.js (์‹œ๊ฐํ™”์šฉ)
  4. ๋ฐฐํฌ: Heroku ๋˜๋Š” PythonAnywhere (์ดˆ๋ณด์ž ์นœํ™”์ )

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

2. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •ํ•˜๊ธฐ ๐Ÿ› ๏ธ

์ž, ์ด์ œ ์ง„์งœ ์ฝ”๋”ฉ์„ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ์š”? ๋จผ์ € ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๋ถ€ํ„ฐ ์„ธํŒ…ํ•ด๋ด…์‹œ๋‹ค!

2.1 Python ์„ค์น˜ํ•˜๊ธฐ

๋‹น์—ฐํžˆ Python์ด ํ•„์š”ํ•˜๊ฒ ์ฃ ? ์ตœ์‹  ๋ฒ„์ „(3.8 ์ด์ƒ)์„ ์„ค์น˜ํ•˜๋Š” ๊ฑธ ์ถ”์ฒœํ•ด์š”.

Python ๊ณต์‹ ์‚ฌ์ดํŠธ(https://www.python.org/downloads/)์—์„œ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์œผ์‹ค ์ˆ˜ ์žˆ์–ด์š”. ์„ค์น˜ํ•  ๋•Œ 'Add Python to PATH' ์˜ต์…˜ ๊ผญ ์ฒดํฌํ•˜์„ธ์š”! ์•ˆ ๊ทธ๋Ÿฌ๋ฉด ๋‚˜์ค‘์— ๋จธ๋ฆฌ ์•„ํŒŒ์š”... ใ…‹ใ…‹ใ…‹

2.2 ๊ฐ€์ƒํ™˜๊ฒฝ ๋งŒ๋“ค๊ธฐ

ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ๋ณ„๋„์˜ ํ™˜๊ฒฝ์„ ๋งŒ๋“œ๋Š” ๊ฑด ์ง„์งœ ๊ฟ€ํŒ์ด์—์š”! ๋‚˜์ค‘์— ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ๋ž‘ ํŒจํ‚ค์ง€ ์ถฉ๋Œ ์—†์ด ๊น”๋”ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”.

ํ„ฐ๋ฏธ๋„(๋งฅ/๋ฆฌ๋ˆ…์Šค) ๋˜๋Š” ๋ช…๋ น ํ”„๋กฌํ”„ํŠธ(์œˆ๋„์šฐ)๋ฅผ ์—ด๊ณ  ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด๋ด…์‹œ๋‹ค:


# ํ”„๋กœ์ ํŠธ ํด๋” ๋งŒ๋“ค๊ธฐ
mkdir survey_app
cd survey_app

# ๊ฐ€์ƒํ™˜๊ฒฝ ๋งŒ๋“ค๊ธฐ
python -m venv venv

# ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” (์œˆ๋„์šฐ)
venv\Scripts\activate

# ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” (๋งฅ/๋ฆฌ๋ˆ…์Šค)
source venv/bin/activate
    

๊ฐ€์ƒํ™˜๊ฒฝ์ด ํ™œ์„ฑํ™”๋˜๋ฉด ํ„ฐ๋ฏธ๋„ ์•ž์— (venv)๋ผ๊ณ  ํ‘œ์‹œ๋  ๊ฑฐ์˜ˆ์š”. ์ด์ œ ์ด ํ™˜๊ฒฝ ์•ˆ์—์„œ๋งŒ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค! ๐Ÿ‘

2.3 ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜ํ•˜๊ธฐ

์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ์— ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋“ค์„ ์„ค์น˜ํ•ด๋ณผ๊ฒŒ์š”:


# ๊ธฐ๋ณธ ์›น ํ”„๋ ˆ์ž„์›Œํฌ ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จ
pip install flask flask-sqlalchemy flask-migrate flask-login

# ํผ ์ฒ˜๋ฆฌ ๋ฐ ๋ณด์•ˆ
pip install flask-wtf email_validator

# ์‹œ๊ฐํ™” ๊ด€๋ จ
pip install matplotlib pandas
    

์ด ํŒจํ‚ค์ง€๋“ค์ด ๋ญํ•˜๋Š” ๊ฑด์ง€ ๊ฐ„๋‹จํžˆ ์„ค๋ช…๋“œ๋ฆด๊ฒŒ์š”:

  1. Flask: ํŒŒ์ด์ฌ ์›น ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๋ํŒ์™•! ๊ฐ€๋ณ๊ณ  ํ™•์žฅ์„ฑ ์ข‹์Œ
  2. Flask-SQLAlchemy: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‰ฝ๊ฒŒ ๋‹ค๋ฃจ๋Š” ORM
  3. Flask-Migrate: DB ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ๊ด€๋ฆฌ ๋„๊ตฌ
  4. Flask-Login: ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ/์ธ์ฆ ๊ด€๋ฆฌ
  5. Flask-WTF: ํผ ์ฒ˜๋ฆฌ ๋ฐ CSRF ๋ณด์•ˆ
  6. Matplotlib & Pandas: ๋ฐ์ดํ„ฐ ๋ถ„์„ ๋ฐ ์‹œ๊ฐํ™”

์ด ์ •๋„๋ฉด ๊ธฐ๋ณธ์ ์ธ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๊ธฐ์— ์ถฉ๋ถ„ํ•ด์š”! ๐Ÿ˜Ž

2.4 ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ

์ด์ œ ํ”„๋กœ์ ํŠธ ํด๋” ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. ๊น”๋”ํ•œ ๊ตฌ์กฐ๋Š” ๋‚˜์ค‘์— ์ฝ”๋“œ ๊ด€๋ฆฌํ•  ๋•Œ ์ง„์งœ ์ค‘์š”ํ•ด์š”!


survey_app/
  โ”œโ”€โ”€ venv/                 # ๊ฐ€์ƒํ™˜๊ฒฝ (์ด๋ฏธ ๋งŒ๋“ค์—ˆ์ฃ ?)
  โ”œโ”€โ”€ app/                  # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ
  โ”‚   โ”œโ”€โ”€ __init__.py       # ์•ฑ ์ดˆ๊ธฐํ™”
  โ”‚   โ”œโ”€โ”€ models.py         # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ
  โ”‚   โ”œโ”€โ”€ routes.py         # ๋ผ์šฐํŠธ (URL ์ฒ˜๋ฆฌ)
  โ”‚   โ”œโ”€โ”€ forms.py          # ํผ ํด๋ž˜์Šค
  โ”‚   โ”œโ”€โ”€ static/           # CSS, JS, ์ด๋ฏธ์ง€ ๋“ฑ
  โ”‚   โ””โ”€โ”€ templates/        # HTML ํ…œํ”Œ๋ฆฟ
  โ”œโ”€โ”€ migrations/           # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
  โ”œโ”€โ”€ config.py             # ์„ค์ • ํŒŒ์ผ
  โ”œโ”€โ”€ run.py                # ์•ฑ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ
  โ””โ”€โ”€ requirements.txt      # ํŒจํ‚ค์ง€ ๋ชฉ๋ก
    

์ด ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ผ ํด๋”์™€ ํŒŒ์ผ๋“ค์„ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”. ํ„ฐ๋ฏธ๋„์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”:


# app ํด๋”์™€ ๊ธฐ๋ณธ ํŒŒ์ผ ์ƒ์„ฑ
mkdir -p app/static app/templates
touch app/__init__.py app/models.py app/routes.py app/forms.py
touch config.py run.py requirements.txt
    

์ž, ์ด์ œ ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ€ ์™„์„ฑ๋์–ด์š”! ๋‹ค์Œ์œผ๋กœ Flask ์•ฑ์˜ ๋ผˆ๋Œ€๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”.

3. Flask๋กœ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ผˆ๋Œ€ ๋งŒ๋“ค๊ธฐ ๐Ÿ—๏ธ

์ด์ œ ์ง„์งœ ์ฝ”๋”ฉ ์‹œ์ž‘์ด์—์š”! Flask๋กœ ์›น ์•ฑ์˜ ๊ธฐ๋ณธ ๋ผˆ๋Œ€๋ฅผ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค.

3.1 Flask ์•ฑ ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ

๋จผ์ € app/__init__.py ํŒŒ์ผ์— Flask ์•ฑ์„ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ๊ฒŒ์š”:


from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from config import Config

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ์ฒด ์ƒ์„ฑ
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)
    
    # ํ™•์žฅ ๊ธฐ๋Šฅ ์ดˆ๊ธฐํ™”
    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)
    
    # ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ๋“ฑ๋ก
    from app.main import bp as main_bp
    app.register_blueprint(main_bp)
    
    from app.auth import bp as auth_bp
    app.register_blueprint(auth_bp, url_prefix='/auth')
    
    from app.survey import bp as survey_bp
    app.register_blueprint(survey_bp, url_prefix='/survey')
    
    return app

from app import models  # ๋ชจ๋ธ ์ž„ํฌํŠธ
    

์šฐ์™€, ๋ญ”๊ฐ€ ๋ณต์žกํ•ด ๋ณด์ด์ฃ ? ใ…‹ใ…‹ใ…‹ ํ•˜์ง€๋งŒ ๊ฑฑ์ • ๋งˆ์„ธ์š”! ํ•˜๋‚˜์”ฉ ์„ค๋ช…ํ•ด๋“œ๋ฆด๊ฒŒ์š”:

  1. ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋“ค์„ ์ž„ํฌํŠธํ–ˆ์–ด์š”
  2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜, ๋กœ๊ทธ์ธ ๊ด€๋ฆฌ์ž ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ–ˆ์–ด์š”
  3. create_app ํ•จ์ˆ˜๋กœ ์•ฑ์„ ๋งŒ๋“ค๊ณ  ์„ค์ •์„ ์ ์šฉํ–ˆ์–ด์š”
  4. ๋ธ”๋ฃจํ”„๋ฆฐํŠธ๋ผ๋Š” ๊ฑธ ๋“ฑ๋กํ–ˆ๋Š”๋ฐ, ์ด๊ฑด ์•ฑ์„ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋‚˜๋ˆ„๋Š” ๋ฐฉ๋ฒ•์ด์—์š”

์ด๋ ‡๊ฒŒ ์•ฑ์„ ํ•จ์ˆ˜๋กœ ๋งŒ๋“œ๋Š” ํŒจํ„ด์„ '์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒฉํ† ๋ฆฌ'๋ผ๊ณ  ํ•ด์š”. ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์ข‹๊ณ  ํ™•์žฅ์„ฑ๋„ ์ข‹์•„์„œ ์‹ค๋ฌด์—์„œ ๋งŽ์ด ์“ฐ๋Š” ๋ฐฉ์‹์ด์—์š”!

3.2 ์„ค์ • ํŒŒ์ผ ๋งŒ๋“ค๊ธฐ

์ด์ œ config.py ํŒŒ์ผ์— ์•ฑ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”:


import os
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or '์ ˆ๋Œ€-๋“คํ‚ค๋ฉด-์•ˆ๋˜๋Š”-๋น„๋ฐ€ํ‚ค'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    

์ด ์„ค์ • ํŒŒ์ผ์€ ๋‘ ๊ฐ€์ง€ ์ค‘์š”ํ•œ ์„ค์ •์„ ๋‹ด๊ณ  ์žˆ์–ด์š”:

  1. SECRET_KEY: ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™”์— ์‚ฌ์šฉ๋˜๋Š” ๋น„๋ฐ€ ํ‚ค
  2. SQLALCHEMY_DATABASE_URI: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ณด

์‹ค์ œ ๋ฐฐํฌํ•  ๋•Œ๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์ด๋Ÿฐ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”. ์ง€๊ธˆ์€ ๊ฐœ๋ฐœ์šฉ์œผ๋กœ ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ•ด๋’€์–ด์š”.

3.3 ๋ธ”๋ฃจํ”„๋ฆฐํŠธ ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ

์œ„์—์„œ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ๋ฅผ ๋“ฑ๋กํ–ˆ๋Š”๋ฐ, ์ด์ œ ๊ทธ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. ๋ธ”๋ฃจํ”„๋ฆฐํŠธ๋Š” ์•ฑ์„ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ชจ๋“ˆํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์—์š”.


# ํด๋” ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ
mkdir -p app/main app/auth app/survey
touch app/main/__init__.py app/main/routes.py
touch app/auth/__init__.py app/auth/routes.py app/auth/forms.py
touch app/survey/__init__.py app/survey/routes.py app/survey/forms.py
    

๊ฐ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ์˜ __init__.py ํŒŒ์ผ์— ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


# app/main/__init__.py
from flask import Blueprint

bp = Blueprint('main', __name__)

from app.main import routes
    

๋‹ค๋ฅธ ๋ธ”๋ฃจํ”„๋ฆฐํŠธ๋„ ๋น„์Šทํ•˜๊ฒŒ ์„ค์ •ํ•˜๋˜, ์ด๋ฆ„๋งŒ 'auth'๋‚˜ 'survey'๋กœ ๋ฐ”๊ฟ”์ฃผ์„ธ์š”.

3.4 ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ ๋งŒ๋“ค๊ธฐ

run.py ํŒŒ์ผ์— ์•ฑ์„ ์‹คํ–‰ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”:


from app import create_app, db
from app.models import User, Survey, Question, Option, Response

app = create_app()

@app.shell_context_processor
def make_shell_context():
    return {
        'db': db, 
        'User': User,
        'Survey': Survey,
        'Question': Question,
        'Option': Option,
        'Response': Response
    }

if __name__ == '__main__':
    app.run(debug=True)
    

์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ๋‘ ๊ฐ€์ง€ ์—ญํ• ์„ ํ•ด์š”:

  1. ์•ฑ์„ ์ƒ์„ฑํ•˜๊ณ  ์‹คํ–‰ํ•ด์š”
  2. Flask ์‰˜์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ปจํ…์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•ด์š” (๋””๋ฒ„๊น…ํ•  ๋•Œ ์œ ์šฉํ•ด์š”)

์ด์ œ ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ€ ์™„์„ฑ๋์–ด์š”! ๋‹ค์Œ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ์„ ์„ค๊ณ„ํ•ด๋ณผ๊ฒŒ์š”.

4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์—ฐ๋™ํ•˜๊ธฐ ๐Ÿ’พ

์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์˜ ์‹ฌ์žฅ์€ ๋ฐ”๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ˆ์š”! ์–ด๋–ค ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์ €์žฅํ• ์ง€ ์ž˜ ์„ค๊ณ„ํ•ด์•ผ ๋‚˜์ค‘์— ๊ณ ์ƒ ์•ˆ ํ•ด์š”~

4.1 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ ์„ค๊ณ„ํ•˜๊ธฐ

์šฐ๋ฆฌ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์ƒ๊ฐํ•ด๋ณผ๊นŒ์š”?

  1. User: ์‚ฌ์šฉ์ž ์ •๋ณด (์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ)
  2. Survey: ์„ค๋ฌธ์ง€ ์ •๋ณด (์ œ๋ชฉ, ์„ค๋ช…, ์ƒ์„ฑ์ž ๋“ฑ)
  3. Question: ์งˆ๋ฌธ ์ •๋ณด (์งˆ๋ฌธ ๋‚ด์šฉ, ์œ ํ˜• ๋“ฑ)
  4. Option: ๊ฐ๊ด€์‹ ์งˆ๋ฌธ์˜ ์„ ํƒ์ง€
  5. Response: ์‚ฌ์šฉ์ž ์‘๋‹ต ๋ฐ์ดํ„ฐ

์ด ๋ชจ๋ธ๋“ค ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ๋‹ค์ด์–ด๊ทธ๋žจ์œผ๋กœ ํ‘œํ˜„ํ•ด๋ณผ๊ฒŒ์š”:

User id: Integer (PK) username: String email: String password_hash: String Survey id: Integer (PK) title: String description: Text user_id: Integer (FK) Question id: Integer (PK) content: Text type: String survey_id: Integer (FK) Option id: Integer (PK) text: String question_id: Integer (FK) Response id: Integer (PK) answer: Text question_id: Integer (FK) respondent_id: Integer 1:N 1:N 1:N 1:N ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ

์ด์ œ ์ด ๋ชจ๋ธ๋“ค์„ ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ์š”! app/models.py ํŒŒ์ผ์— ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”:


from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app import db, login

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))
    surveys = db.relationship('Survey', backref='author', lazy='dynamic')
    
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)
        
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    def __repr__(self):
        return f'<User {self.username}>'

@login.user_loader
def load_user(id):
    return User.query.get(int(id))

class Survey(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    description = db.Column(db.Text)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    is_active = db.Column(db.Boolean, default=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    questions = db.relationship('Question', backref='survey', lazy='dynamic', 
                               cascade='all, delete-orphan')
    
    def __repr__(self):
        return f'<Survey {self.title}>'

class Question(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    type = db.Column(db.String(20), nullable=False)  # 'text', 'radio', 'checkbox', 'dropdown'
    required = db.Column(db.Boolean, default=False)
    order = db.Column(db.Integer, default=0)
    survey_id = db.Column(db.Integer, db.ForeignKey('survey.id'))
    options = db.relationship('Option', backref='question', lazy='dynamic', 
                             cascade='all, delete-orphan')
    responses = db.relationship('Response', backref='question', lazy='dynamic', 
                               cascade='all, delete-orphan')
    
    def __repr__(self):
        return f'<Question {self.id}: {self.content[:20]}...>'

class Option(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(200), nullable=False)
    order = db.Column(db.Integer, default=0)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id'))
    
    def __repr__(self):
        return f'<Option {self.id}: {self.text}>'

class Response(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    answer = db.Column(db.Text)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id'))
    respondent_id = db.Column(db.String(36))  # UUID for anonymous respondents
    
    def __repr__(self):
        return f'<Response {self.id} to question {self.question_id}>'
    

์šฐ์™€, ์ฝ”๋“œ๊ฐ€ ์ข€ ๊ธธ์ฃ ? ใ…‹ใ…‹ใ…‹ ํ•˜์ง€๋งŒ ๊ฐ ๋ชจ๋ธ์ด ํ•˜๋Š” ์ผ์€ ๊ฐ„๋‹จํ•ด์š”:

  1. User: ์‚ฌ์šฉ์ž ์ •๋ณด์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ ๊ธฐ๋Šฅ
  2. Survey: ์„ค๋ฌธ์ง€ ๊ธฐ๋ณธ ์ •๋ณด
  3. Question: ์งˆ๋ฌธ ๋‚ด์šฉ๊ณผ ์œ ํ˜•
  4. Option: ๊ฐ๊ด€์‹ ์งˆ๋ฌธ์˜ ์„ ํƒ์ง€
  5. Response: ์‘๋‹ต์ž๋“ค์˜ ๋‹ต๋ณ€

SQLAlchemy์˜ relationship ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ด์„œ ๋ชจ๋ธ ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ–ˆ์–ด์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‚˜์ค‘์— 'survey.questions'์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๊ด€๋ จ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์š”!

4.2 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ

์ด์ œ ๋ชจ๋ธ์„ ๋งŒ๋“ค์—ˆ์œผ๋‹ˆ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด๋ณผ๊ฒŒ์š”. ํ„ฐ๋ฏธ๋„์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”:


# Flask ์•ฑ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • (์œˆ๋„์šฐ)
set FLASK_APP=run.py

# Flask ์•ฑ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • (๋งฅ/๋ฆฌ๋ˆ…์Šค)
export FLASK_APP=run.py

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
    

์ด ๋ช…๋ น์–ด๋“ค์ด ํ•˜๋Š” ์ผ์€:

  1. flask db init: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™”
  2. flask db migrate: ๋ชจ๋ธ ๋ณ€๊ฒฝ์‚ฌํ•ญ ๊ฐ์ง€ํ•ด์„œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ
  3. flask db upgrade: ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ ์šฉ

์ด์ œ app.db ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์—ˆ์„ ๊ฑฐ์˜ˆ์š”. ์ด๊ฒŒ ๋ฐ”๋กœ ์šฐ๋ฆฌ์˜ SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ˆ์š”! ๐ŸŽ‰

4.3 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์ œ๋Œ€๋กœ ์„ค์ •๋๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด๋ณผ๊นŒ์š”? Flask ์‰˜์„ ์‚ฌ์šฉํ•˜๋ฉด ํŽธํ•ด์š”:


# Flask ์‰˜ ์‹คํ–‰
flask shell

# ์‰˜์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด ์‹คํ–‰
>>> user = User(username='test_user', email='test@example.com')
>>> user.set_password('password123')
>>> db.session.add(user)
>>> db.session.commit()
>>> User.query.all()
[<User test_user>]
>>> exit()
    

์˜ค ์ž˜ ๋˜๋„ค์š”! ์ด์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •์ด ์™„๋ฃŒ๋์–ด์š”. ๋‹ค์Œ์œผ๋กœ ์„ค๋ฌธ ์ƒ์„ฑ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ์š”!

5. ์„ค๋ฌธ ์ƒ์„ฑ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ“

์ด์ œ ์ง„์งœ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ธ ์„ค๋ฌธ ์ƒ์„ฑ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”! ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค์–‘ํ•œ ์œ ํ˜•์˜ ์งˆ๋ฌธ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์„ค๋ฌธ์ง€๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๋ด…์‹œ๋‹ค.

5.1 ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌํ˜„ํ•˜๊ธฐ

์„ค๋ฌธ์„ ๋งŒ๋“ค๋ ค๋ฉด ๋จผ์ € ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•˜๊ฒ ์ฃ ? app/auth/forms.py ํŒŒ์ผ์— ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ํผ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”:


from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
from app.models import User

class LoginForm(FlaskForm):
    email = StringField('์ด๋ฉ”์ผ', validators=[DataRequired(), Email()])
    password = PasswordField('๋น„๋ฐ€๋ฒˆํ˜ธ', validators=[DataRequired()])
    remember_me = BooleanField('๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€')
    submit = SubmitField('๋กœ๊ทธ์ธ')

class RegistrationForm(FlaskForm):
    username = StringField('์‚ฌ์šฉ์ž ์ด๋ฆ„', validators=[DataRequired()])
    email = StringField('์ด๋ฉ”์ผ', validators=[DataRequired(), Email()])
    password = PasswordField('๋น„๋ฐ€๋ฒˆํ˜ธ', validators=[DataRequired()])
    password2 = PasswordField(
        '๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('ํšŒ์›๊ฐ€์ž…')
    
    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError('์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์‚ฌ์šฉ์ž ์ด๋ฆ„์ž…๋‹ˆ๋‹ค.')
            
    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.')
    

์ด์ œ app/auth/routes.py ํŒŒ์ผ์— ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”:


from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user
from werkzeug.urls import url_parse
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User

@bp.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค')
            return redirect(url_for('auth.login'))
        login_user(user, remember=form.remember_me.data)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('main.index')
        return redirect(next_page)
    return render_template('auth/login.html', title='๋กœ๊ทธ์ธ', form=form)

@bp.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('main.index'))

@bp.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', title='ํšŒ์›๊ฐ€์ž…', form=form)
    

์ด์ œ app/templates/auth ํด๋”๋ฅผ ๋งŒ๋“ค๊ณ  ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ํ…œํ”Œ๋ฆฟ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด์š”. ๋จผ์ € ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”.

app/templates/base.html:


<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% if title %}{{ title }} - {% endif %}์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container">
            <a class="navbar-brand" href="{{ url_for('main.index') }}">์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('main.index') }}">ํ™ˆ</a>
                    </li>
                    {% if current_user.is_authenticated %}
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('survey.create') }}">์„ค๋ฌธ ๋งŒ๋“ค๊ธฐ</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('survey.my_surveys') }}">๋‚ด ์„ค๋ฌธ</a>
                    </li>
                    {% endif %}
                </ul>
                <ul class="navbar-nav">
                    {% if current_user.is_authenticated %}
                    <li class="nav-item">
                        <span class="nav-link">{{ current_user.username }}</span>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.logout') }}">๋กœ๊ทธ์•„์›ƒ</a>
                    </li>
                    {% else %}
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.login') }}">๋กœ๊ทธ์ธ</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.register') }}">ํšŒ์›๊ฐ€์ž…</a>
                    </li>
                    {% endif %}
                </ul>
            </div>
        </div>
    </nav>
    
    <div class="container mt-4">
        {% with messages = get_flashed_messages() %}
        {% if messages %}
        <div class="row">
            <div class="col-md-12">
                {% for message in messages %}
                <div class="alert alert-info">{{ message }}</div>
                {% endfor %}
            </div>
        </div>
        {% endif %}
        {% endwith %}
        
        {% block content %}{% endblock %}
    </div>
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    {% block scripts %}{% endblock %}
</body>
</html>
    

์ด์ œ ๋กœ๊ทธ์ธ ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/auth/login.html:


{% extends "base.html" %}

{% block content %}
<div class="row">
    <div class="col-md-6 offset-md-3">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title">๋กœ๊ทธ์ธ</h3>
            </div>
            <div class="card-body">
                <form action="" method="post" novalidate>
                    {{ form.hidden_tag() }}
                    <div class="mb-3">
                        {{ form.email.label(class="form-label") }}
                        {{ form.email(class="form-control") }}
                        {% for error in form.email.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ form.password.label(class="form-label") }}
                        {{ form.password(class="form-control") }}
                        {% for error in form.password.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3 form-check">
                        {{ form.remember_me(class="form-check-input") }}
                        {{ form.remember_me.label(class="form-check-label") }}
                    </div>
                    <div class="mb-3">
                        {{ form.submit(class="btn btn-primary") }}
                    </div>
                </form>
                <p>๊ณ„์ •์ด ์—†์œผ์‹ ๊ฐ€์š”? <a href="{{ url_for('auth.register') }}">ํšŒ์›๊ฐ€์ž…</a></p>
            </div>
        </div>
    </div>
</div>
{% endblock %}
    

ํšŒ์›๊ฐ€์ž… ํ…œํ”Œ๋ฆฟ๋„ ๋น„์Šทํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”. ์ด์ œ ๋ฉ”์ธ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/main/routes.py:


from flask import render_template, redirect, url_for
from app.main import bp

@bp.route('/')
@bp.route('/index')
def index():
    return render_template('index.html', title='ํ™ˆ')
    

app/templates/index.html:


{% extends "base.html" %}

{% block content %}
<div class="jumbotron">
    <h1 class="display-4">๋‚˜๋งŒ์˜ ์„ค๋ฌธ์กฐ์‚ฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”!</h1>
    <p class="lead">๊ฐ„๋‹จํ•˜๊ณ  ๊ฐ•๋ ฅํ•œ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ๋ถ„์„ํ•ด๋ณด์„ธ์š”.</p>
    <hr class="my-4">
    <p>์ง€๊ธˆ ๋ฐ”๋กœ ์‹œ์ž‘ํ•˜์„ธ์š”!</p>
    {% if current_user.is_authenticated %}
    <a class="btn btn-primary btn-lg" href="{{ url_for('survey.create') }}">์„ค๋ฌธ ๋งŒ๋“ค๊ธฐ</a>
    {% else %}
    <a class="btn btn-primary btn-lg" href="{{ url_for('auth.register') }}">ํšŒ์›๊ฐ€์ž…</a>
    <a class="btn btn-outline-primary btn-lg" href="{{ url_for('auth.login') }}">๋กœ๊ทธ์ธ</a>
    {% endif %}
</div>

<div class="row mt-5">
    <div class="col-md-4">
        <h2>์‰ฝ๊ณ  ๋น ๋ฅธ ์„ค๋ฌธ ์ œ์ž‘</h2>
        <p>๋‹ค์–‘ํ•œ ์งˆ๋ฌธ ์œ ํ˜•๊ณผ ์ง๊ด€์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋ช‡ ๋ถ„ ์•ˆ์— ์ „๋ฌธ์ ์ธ ์„ค๋ฌธ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
    </div>
    <div class="col-md-4">
        <h2>์‹ค์‹œ๊ฐ„ ์‘๋‹ต ํ™•์ธ</h2>
        <p>์‘๋‹ต์ด ๋“ค์–ด์˜ค๋Š” ์ฆ‰์‹œ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๊ณ  ์‹œ๊ฐ์  ๋Œ€์‹œ๋ณด๋“œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
    </div>
    <div class="col-md-4">
        <h2>์–ด๋””์„œ๋‚˜ ์ ‘๊ทผ ๊ฐ€๋Šฅ</h2>
        <p>๋ชจ๋ฐ”์ผ ์นœํ™”์ ์ธ ๋””์ž์ธ์œผ๋กœ ์–ด๋–ค ๊ธฐ๊ธฐ์—์„œ๋“  ์„ค๋ฌธ์— ์‘๋‹ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
    </div>
</div>
{% endblock %}
    

5.2 ์„ค๋ฌธ ์ƒ์„ฑ ํผ ๋งŒ๋“ค๊ธฐ

์ด์ œ ์„ค๋ฌธ์„ ๋งŒ๋“œ๋Š” ํผ์„ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ์š”. app/survey/forms.py:


from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length

class SurveyForm(FlaskForm):
    title = StringField('์„ค๋ฌธ ์ œ๋ชฉ', validators=[DataRequired(), Length(max=100)])
    description = TextAreaField('์„ค๋ฌธ ์„ค๋ช…')
    is_active = BooleanField('ํ™œ์„ฑํ™”', default=True)
    submit = SubmitField('์ €์žฅ')

class QuestionForm(FlaskForm):
    content = TextAreaField('์งˆ๋ฌธ ๋‚ด์šฉ', validators=[DataRequired()])
    type = SelectField('์งˆ๋ฌธ ์œ ํ˜•', choices=[
        ('text', '์ฃผ๊ด€์‹ (ํ…์ŠคํŠธ)'),
        ('radio', '๊ฐ๊ด€์‹ (๋‹จ์ผ ์„ ํƒ)'),
        ('checkbox', '๊ฐ๊ด€์‹ (๋‹ค์ค‘ ์„ ํƒ)'),
        ('dropdown', '๋“œ๋กญ๋‹ค์šด')
    ], validators=[DataRequired()])
    required = BooleanField('ํ•„์ˆ˜ ์‘๋‹ต', default=False)
    submit = SubmitField('์งˆ๋ฌธ ์ถ”๊ฐ€')

class OptionForm(FlaskForm):
    text = StringField('์„ ํƒ์ง€ ํ…์ŠคํŠธ', validators=[DataRequired()])
    submit = SubmitField('์„ ํƒ์ง€ ์ถ”๊ฐ€')
    

์ด์ œ app/survey/routes.py ํŒŒ์ผ์— ์„ค๋ฌธ ์ƒ์„ฑ ๊ด€๋ จ ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”:


from flask import render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from app import db
from app.survey import bp
from app.survey.forms import SurveyForm, QuestionForm, OptionForm
from app.models import Survey, Question, Option

@bp.route('/create', methods=['GET', 'POST'])
@login_required
def create():
    form = SurveyForm()
    if form.validate_on_submit():
        survey = Survey(
            title=form.title.data,
            description=form.description.data,
            is_active=form.is_active.data,
            author=current_user
        )
        db.session.add(survey)
        db.session.commit()
        flash('์„ค๋ฌธ์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ์ด์ œ ์งˆ๋ฌธ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.')
        return redirect(url_for('survey.edit', id=survey.id))
    return render_template('survey/create.html', title='์„ค๋ฌธ ๋งŒ๋“ค๊ธฐ', form=form)

@bp.route('/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit(id):
    survey = Survey.query.get_or_404(id)
    if survey.author != current_user:
        flash('์ด ์„ค๋ฌธ์„ ํŽธ์ง‘ํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('main.index'))
    
    # ์„ค๋ฌธ ๊ธฐ๋ณธ ์ •๋ณด ์ˆ˜์ • ํผ
    survey_form = SurveyForm(obj=survey)
    if survey_form.validate_on_submit():
        survey_form.populate_obj(survey)
        db.session.commit()
        flash('์„ค๋ฌธ์ด ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('survey.edit', id=survey.id))
    
    # ์งˆ๋ฌธ ์ถ”๊ฐ€ ํผ
    question_form = QuestionForm()
    
    # ์งˆ๋ฌธ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
    questions = survey.questions.order_by(Question.order).all()
    
    return render_template('survey/edit.html', 
                          title='์„ค๋ฌธ ํŽธ์ง‘',
                          survey=survey,
                          survey_form=survey_form,
                          question_form=question_form,
                          questions=questions)

@bp.route('/add_question/<int:survey_id>', methods=['POST'])
@login_required
def add_question(survey_id):
    survey = Survey.query.get_or_404(survey_id)
    if survey.author != current_user:
        flash('์ด ์„ค๋ฌธ์— ์งˆ๋ฌธ์„ ์ถ”๊ฐ€ํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('main.index'))
    
    form = QuestionForm()
    if form.validate_on_submit():
        # ํ˜„์žฌ ์งˆ๋ฌธ ๊ฐœ์ˆ˜ ํ™•์ธํ•ด์„œ ์ˆœ์„œ ์ •ํ•˜๊ธฐ
        order = survey.questions.count()
        
        question = Question(
            content=form.content.data,
            type=form.type.data,
            required=form.required.data,
            order=order,
            survey=survey
        )
        db.session.add(question)
        db.session.commit()
        
        flash('์งˆ๋ฌธ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
        
        # ๊ฐ๊ด€์‹ ์งˆ๋ฌธ์ด๋ฉด ์„ ํƒ์ง€ ์ถ”๊ฐ€ ํŽ˜์ด์ง€๋กœ ์ด๋™
        if question.type in ['radio', 'checkbox', 'dropdown']:
            return redirect(url_for('survey.add_options', question_id=question.id))
    
    return redirect(url_for('survey.edit', id=survey_id))

@bp.route('/add_options/<int:question_id>', methods=['GET', 'POST'])
@login_required
def add_options(question_id):
    question = Question.query.get_or_404(question_id)
    survey = question.survey
    
    if survey.author != current_user:
        flash('์ด ์งˆ๋ฌธ์— ์„ ํƒ์ง€๋ฅผ ์ถ”๊ฐ€ํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('main.index'))
    
    form = OptionForm()
    if form.validate_on_submit():
        # ํ˜„์žฌ ์„ ํƒ์ง€ ๊ฐœ์ˆ˜ ํ™•์ธํ•ด์„œ ์ˆœ์„œ ์ •ํ•˜๊ธฐ
        order = question.options.count()
        
        option = Option(
            text=form.text.data,
            order=order,
            question=question
        )
        db.session.add(option)
        db.session.commit()
        
        flash('์„ ํƒ์ง€๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('survey.add_options', question_id=question.id))
    
    options = question.options.order_by(Option.order).all()
    
    return render_template('survey/add_options.html',
                          title='์„ ํƒ์ง€ ์ถ”๊ฐ€',
                          question=question,
                          survey=survey,
                          form=form,
                          options=options)

@bp.route('/my_surveys')
@login_required
def my_surveys():
    surveys = current_user.surveys.order_by(Survey.created_at.desc()).all()
    return render_template('survey/my_surveys.html', title='๋‚ด ์„ค๋ฌธ', surveys=surveys)
    

์ด์ œ ์„ค๋ฌธ ์ƒ์„ฑ ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/survey/create.html:


{% extends "base.html" %}

{% block content %}
<div class="row">
    <div class="col-md-8 offset-md-2">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title">์ƒˆ ์„ค๋ฌธ ๋งŒ๋“ค๊ธฐ</h3>
            </div>
            <div class="card-body">
                <form action="" method="post" novalidate>
                    {{ form.hidden_tag() }}
                    <div class="mb-3">
                        {{ form.title.label(class="form-label") }}
                        {{ form.title(class="form-control") }}
                        {% for error in form.title.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ form.description.label(class="form-label") }}
                        {{ form.description(class="form-control", rows=4) }}
                        {% for error in form.description.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3 form-check">
                        {{ form.is_active(class="form-check-input") }}
                        {{ form.is_active.label(class="form-check-label") }}
                    </div>
                    <div class="mb-3">
                        {{ form.submit(class="btn btn-primary") }}
                        <a href="{{ url_for('main.index') }}" class="btn btn-secondary">์ทจ์†Œ</a>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}
    

์„ค๋ฌธ ํŽธ์ง‘ ํŽ˜์ด์ง€๋„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/survey/edit.html:


{% extends "base.html" %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-8">
        <h2>{{ survey.title }} <small class="text-muted">ํŽธ์ง‘</small></h2>
        <p>{{ survey.description }}</p>
    </div>
    <div class="col-md-4 text-end">
        <a href="{{ url_for('survey.my_surveys') }}" class="btn btn-outline-secondary">๋‚ด ์„ค๋ฌธ ๋ชฉ๋ก</a>
    </div>
</div>

<div class="row">
    <div class="col-md-4">
        <div class="card mb-4">
            <div class="card-header">
                <h4 class="card-title">์„ค๋ฌธ ์ •๋ณด ์ˆ˜์ •</h4>
            </div>
            <div class="card-body">
                <form action="" method="post" novalidate>
                    {{ survey_form.hidden_tag() }}
                    <div class="mb-3">
                        {{ survey_form.title.label(class="form-label") }}
                        {{ survey_form.title(class="form-control") }}
                        {% for error in survey_form.title.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ survey_form.description.label(class="form-label") }}
                        {{ survey_form.description(class="form-control", rows=4) }}
                        {% for error in survey_form.description.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3 form-check">
                        {{ survey_form.is_active(class="form-check-input") }}
                        {{ survey_form.is_active.label(class="form-check-label") }}
                    </div>
                    <div class="mb-3">
                        {{ survey_form.submit(class="btn btn-primary") }}
                    </div>
                </form>
            </div>
        </div>
        
        <div class="card">
            <div class="card-header">
                <h4 class="card-title">์งˆ๋ฌธ ์ถ”๊ฐ€</h4>
            </div>
            <div class="card-body">
                <form action="{{ url_for('survey.add_question', survey_id=survey.id) }}" method="post" novalidate>
                    {{ question_form.hidden_tag() }}
                    <div class="mb-3">
                        {{ question_form.content.label(class="form-label") }}
                        {{ question_form.content(class="form-control", rows=3) }}
                        {% for error in question_form.content.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ question_form.type.label(class="form-label") }}
                        {{ question_form.type(class="form-select") }}
                        {% for error in question_form.type.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3 form-check">
                        {{ question_form.required(class="form-check-input") }}
                        {{ question_form.required.label(class="form-check-label") }}
                    </div>
                    <div class="mb-3">
                        {{ question_form.submit(class="btn btn-success") }}
                    </div>
                </form>
            </div>
        </div>
    </div>
    
    <div class="col-md-8">
        <div class="card">
            <div class="card-header d-flex justify-content-between align-items-center">
                <h4 class="card-title mb-0">์งˆ๋ฌธ ๋ชฉ๋ก</h4>
                <span class="badge bg-primary">{{ questions|length }}๊ฐœ ์งˆ๋ฌธ</span>
            </div>
            <div class="card-body">
                {% if questions %}
                <div class="list-group">
                    {% for question in questions %}
                    <div class="list-group-item">
                        <div class="d-flex justify-content-between align-items-center">
                            <h5 class="mb-1">{{ question.content }}</h5>
                            <span class="badge {% if question.type == 'text' %}bg-info{% elif question.type == 'radio' %}bg-success{% elif question.type == 'checkbox' %}bg-warning{% else %}bg-secondary{% endif %}">
                                {{ question.type }}
                            </span>
                        </div>
                        <p class="mb-1 text-muted">
                            {% if question.required %}ํ•„์ˆ˜ ์‘๋‹ต{% else %}์„ ํƒ ์‘๋‹ต{% endif %}
                        </p>
                        
                        {% if question.type in ['radio', 'checkbox', 'dropdown'] %}
                        <div class="mt-2">
                            <strong>์„ ํƒ์ง€:</strong>
                            {% if question.options.count() > 0 %}
                            <ul class="mb-0">
                                {% for option in question.options %}
                                <li>{{ option.text }}</li>
                                {% endfor %}
                            </ul>
                            {% else %}
                            <p class="text-muted">์„ ํƒ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>
                            {% endif %}
                            <a href="{{ url_for('survey.add_options', question_id=question.id) }}" class="btn btn-sm btn-outline-primary mt-2">์„ ํƒ์ง€ ๊ด€๋ฆฌ</a>
                        </div>
                        {% endif %}
                    </div>
                    {% endfor %}
                </div>
                {% else %}
                <div class="alert alert-info">
                    ์•„์ง ์งˆ๋ฌธ์ด ์—†์Šต๋‹ˆ๋‹ค. ์™ผ์ชฝ ํผ์—์„œ ์งˆ๋ฌธ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.
                </div>
                {% endif %}
                
                {% if questions %}
                <div class="mt-4">
                    <a href="{{ url_for('survey.preview', id=survey.id) }}" class="btn btn-primary">์„ค๋ฌธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ</a>
                    <a href="{{ url_for('survey.share', id=survey.id) }}" class="btn btn-success">์„ค๋ฌธ ๊ณต์œ </a>
                </div>
                {% endif %}
            </div>
        </div>
    </div>
</div>
{% endblock %}
    

์„ ํƒ์ง€ ์ถ”๊ฐ€ ํŽ˜์ด์ง€๋„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/survey/add_options.html:


{% extends "base.html" %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-8">
        <h2>์„ ํƒ์ง€ ๊ด€๋ฆฌ <small class="text-muted">{{ question.content }}</small></h2>
        <p>
            <a href="{{ url_for('survey.edit', id=survey.id) }}">โ† ์„ค๋ฌธ ํŽธ์ง‘์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</a>
        </p>
    </div>
</div>

<div class="row">
    <div class="col-md-5">
        <div class="card">
            <div class="card-header">
                <h4 class="card-title">์„ ํƒ์ง€ ์ถ”๊ฐ€</h4>
            </div>
            <div class="card-body">
                <form action="" method="post" novalidate>
                    {{ form.hidden_tag() }}
                    <div class="mb-3">
                        {{ form.text.label(class="form-label") }}
                        {{ form.text(class="form-control") }}
                        {% for error in form.text.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ form.submit(class="btn btn-success") }}
                    </div>
                </form>
            </div>
        </div>
    </div>
    
    <div class="col-md-7">
        <div class="card">
            <div class="card-header d-flex justify-content-between align-items-center">
                <h4 class="card-title mb-0">์„ ํƒ์ง€ ๋ชฉ๋ก</h4>
                <span class="badge bg-primary">{{ options|length }}๊ฐœ ์„ ํƒ์ง€</span>
            </div>
            <div class="card-body">
                {% if options %}
                <div class="list-group">
                    {% for option in options %}
                    <div class="list-group-item d-flex justify-content-between align-items-center">
                        <span>{{ option.text }}</span>
                        <a href="{{ url_for('survey.delete_option', id=option.id) }}" class="btn btn-sm btn-outline-danger" onclick="return confirm('์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')">์‚ญ์ œ</a>
                    </div>
                    {% endfor %}
                </div>
                {% else %}
                <div class="alert alert-info">
                    ์•„์ง ์„ ํƒ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์™ผ์ชฝ ํผ์—์„œ ์„ ํƒ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.
                </div>
                {% endif %}
                
                {% if options|length >= 2 %}
                <div class="mt-4">
                    <a href="{{ url_for('survey.edit', id=survey.id) }}" class="btn btn-primary">์™„๋ฃŒ</a>
                </div>
                {% else %}
                <div class="alert alert-warning mt-4">
                    ์ตœ์†Œ 2๊ฐœ ์ด์ƒ์˜ ์„ ํƒ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.
                </div>
                {% endif %}
            </div>
        </div>
    </div>
</div>
{% endblock %}
    

๋‚ด ์„ค๋ฌธ ๋ชฉ๋ก ํŽ˜์ด์ง€๋„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/survey/my_surveys.html:


{% extends "base.html" %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-8">
        <h2>๋‚ด ์„ค๋ฌธ ๋ชฉ๋ก</h2>
    </div>
    <div class="col-md-4 text-end">
        <a href="{{ url_for('survey.create') }}" class="btn btn-primary">์ƒˆ ์„ค๋ฌธ ๋งŒ๋“ค๊ธฐ</a>
    </div>
</div>

<div class="row">
    <div class="col-md-12">
        {% if surveys %}
        <div class="card">
            <div class="card-body">
                <div class="table-responsive">
                    <table class="table table-hover">
                        <thead>
                            <tr>
                                <th>์ œ๋ชฉ</th>
                                <th>์ƒ์„ฑ์ผ</th>
                                <th>์งˆ๋ฌธ ์ˆ˜</th>
                                <th>์ƒํƒœ</th>
                                <th>์‘๋‹ต ์ˆ˜</th>
                                <th>์ž‘์—…</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for survey in surveys %}
                            <tr>
                                <td>{{ survey.title }}</td>
                                <td>{{ survey.created_at.strftime('%Y-%m-%d') }}</td>
                                <td>{{ survey.questions.count() }}</td>
                                <td>
                                    {% if survey.is_active %}
                                    <span class="badge bg-success">ํ™œ์„ฑ</span>
                                    {% else %}
                                    <span class="badge bg-secondary">๋น„ํ™œ์„ฑ</span>
                                    {% endif %}
                                </td>
                                <td>
                                    {% set response_count = namespace(value=0) %}
                                    {% for question in survey.questions %}
                                        {% set response_count.value = response_count.value + question.responses.count() %}
                                    {% endfor %}
                                    {{ response_count.value }}
                                </td>
                                <td>
                                    <div class="btn-group">
                                        <a href="{{ url_for('survey.edit', id=survey.id) }}" class="btn btn-sm btn-outline-primary">ํŽธ์ง‘</a>
                                        <a href="{{ url_for('survey.share', id=survey.id) }}" class="btn btn-sm btn-outline-success">๊ณต์œ </a>
                                        <a href="{{ url_for('survey.results', id=survey.id) }}" class="btn btn-sm btn-outline-info">๊ฒฐ๊ณผ</a>
                                    </div>
                                </td>
                            </tr>
                            {% endfor %}
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
        {% else %}
        <div class="alert alert-info">
            ์•„์ง ๋งŒ๋“  ์„ค๋ฌธ์ด ์—†์Šต๋‹ˆ๋‹ค. ์œ„์˜ '์ƒˆ ์„ค๋ฌธ ๋งŒ๋“ค๊ธฐ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด ์ฒซ ์„ค๋ฌธ์„ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”!
        </div>
        {% endif %}
    </div>
</div>
{% endblock %}
    

์ด์ œ ๊ธฐ๋ณธ์ ์ธ ์„ค๋ฌธ ์ƒ์„ฑ ๊ธฐ๋Šฅ์ด ์™„์„ฑ๋์–ด์š”! ๋‹ค์Œ์œผ๋กœ ์‘๋‹ต ์ˆ˜์ง‘ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”.

6. ์‘๋‹ต ์ˆ˜์ง‘ ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ ๐Ÿ“Š

์ด์ œ ๋งŒ๋“  ์„ค๋ฌธ์— ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”! ์ด ๋ถ€๋ถ„์ด ์ง„์งœ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์ฃ ~ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ตœ๋Œ€ํ•œ ์ข‹๊ฒŒ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค!

6.1 ์„ค๋ฌธ ๊ณต์œ  ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

๋จผ์ € ์„ค๋ฌธ์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/survey/routes.py์— ๋‹ค์Œ ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


import uuid
from datetime import datetime

@bp.route('/share/<int:id>')
@login_required
def share(id):
    survey = Survey.query.get_or_404(id)
    if survey.author != current_user:
        flash('์ด ์„ค๋ฌธ์„ ๊ณต์œ ํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('main.index'))
    
    # ์„ค๋ฌธ ์‘๋‹ต URL ์ƒ์„ฑ
    response_url = url_for('survey.respond', id=survey.id, _external=True)
    
    return render_template('survey/share.html', 
                          title='์„ค๋ฌธ ๊ณต์œ ',
                          survey=survey,
                          response_url=response_url)
    

์ด์ œ ๊ณต์œ  ํŽ˜์ด์ง€ ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/survey/share.html:


{% extends "base.html" %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-8">
        <h2>์„ค๋ฌธ ๊ณต์œ  <small class="text-muted">{{ survey.title }}</small></h2>
        <p>
            <a href="{{ url_for('survey.edit', id=survey.id) }}">โ† ์„ค๋ฌธ ํŽธ์ง‘์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</a>
        </p>
    </div>
</div>

<div class="row">
    <div class="col-md-8 offset-md-2">
        <div class="card">
            <div class="card-header">
                <h4 class="card-title">์„ค๋ฌธ ์‘๋‹ต ๋งํฌ</h4>
            </div>
            <div class="card-body">
                <p>์•„๋ž˜ ๋งํฌ๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ ์‘๋‹ต์ž๋“ค์—๊ฒŒ ๊ณต์œ ํ•˜์„ธ์š”:</p>
                
                <div class="input-group mb-3">
                    <input type="text" class="form-control" id="response-url" value="{{ response_url }}" readonly>
                    <button class="btn btn-outline-primary" type="button" onclick="copyToClipboard()">๋ณต์‚ฌ</button>
                </div>
                
                <div class="alert alert-success mt-4">
                    <h5>QR ์ฝ”๋“œ๋กœ ๊ณต์œ ํ•˜๊ธฐ</h5>
                    <p>์Šค๋งˆํŠธํฐ์œผ๋กœ ์‰ฝ๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก QR ์ฝ”๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:</p>
                    <img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={{ response_url|urlencode }}" alt="QR Code" class="img-thumbnail">
                </div>
                
                <div class="alert alert-info mt-4">
                    <h5>์†Œ์…œ ๋ฏธ๋””์–ด๋กœ ๊ณต์œ ํ•˜๊ธฐ</h5>
                    <div class="d-flex gap-2 mt-3">
                        <a href="https://twitter.com/intent/tweet?url={{ response_url|urlencode }}&text={{ survey.title|urlencode }}" target="_blank" class="btn btn-outline-primary">Twitter</a>
                        <a href="https://www.facebook.com/sharer/sharer.php?u={{ response_url|urlencode }}" target="_blank" class="btn btn-outline-primary">Facebook</a>
                        <a href="mailto:?subject={{ survey.title|urlencode }}&body={{ survey.description|urlencode }}%0A%0A{{ response_url|urlencode }}" class="btn btn-outline-primary">Email</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
function copyToClipboard() {
    var copyText = document.getElementById("response-url");
    copyText.select();
    copyText.setSelectionRange(0, 99999);
    document.execCommand("copy");
    
    alert("๋งํฌ๊ฐ€ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
}
</script>
{% endblock %}
    

6.2 ์„ค๋ฌธ ์‘๋‹ต ํŽ˜์ด์ง€ ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ๋กœ ์‘๋‹ต์„ ๋ฐ›๋Š” ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/survey/routes.py์— ๋‹ค์Œ ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


@bp.route('/respond/<int:id>', methods=['GET', 'POST'])
def respond(id):
    survey = Survey.query.get_or_404(id)
    
    # ๋น„ํ™œ์„ฑํ™”๋œ ์„ค๋ฌธ์ธ์ง€ ํ™•์ธ
    if not survey.is_active:
        flash('์ด ์„ค๋ฌธ์€ ํ˜„์žฌ ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ์ž…๋‹ˆ๋‹ค.')
        return redirect(url_for('main.index'))
    
    # ์งˆ๋ฌธ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
    questions = survey.questions.order_by(Question.order).all()
    
    if not questions:
        flash('์ด ์„ค๋ฌธ์—๋Š” ์งˆ๋ฌธ์ด ์—†์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('main.index'))
    
    if request.method == 'POST':
        # ์‘๋‹ต์ž ID ์ƒ์„ฑ (์ต๋ช… ์‘๋‹ต์„ ์œ„ํ•œ UUID)
        respondent_id = request.form.get('respondent_id')
        if not respondent_id:
            respondent_id = str(uuid.uuid4())
        
        # ๊ฐ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ์‘๋‹ต ์ฒ˜๋ฆฌ
        for question in questions:
            answer = None
            
            if question.type == 'text':
                # ์ฃผ๊ด€์‹ ์‘๋‹ต
                answer = request.form.get(f'question_{question.id}')
            elif question.type == 'radio':
                # ๋‹จ์ผ ์„ ํƒ ์‘๋‹ต
                answer = request.form.get(f'question_{question.id}')
            elif question.type == 'checkbox':
                # ๋‹ค์ค‘ ์„ ํƒ ์‘๋‹ต
                checkbox_answers = request.form.getlist(f'question_{question.id}')
                if checkbox_answers:
                    answer = ','.join(checkbox_answers)
            elif question.type == 'dropdown':
                # ๋“œ๋กญ๋‹ค์šด ์‘๋‹ต
                answer = request.form.get(f'question_{question.id}')
            
            # ํ•„์ˆ˜ ์งˆ๋ฌธ์ธ๋ฐ ์‘๋‹ต์ด ์—†๋Š” ๊ฒฝ์šฐ
            if question.required and not answer:
                flash(f'"{question.content}" ์งˆ๋ฌธ์€ ํ•„์ˆ˜ ์‘๋‹ต์ž…๋‹ˆ๋‹ค.')
                return redirect(url_for('survey.respond', id=survey.id))
            
            # ์‘๋‹ต ์ €์žฅ
            if answer:
                response = Response(
                    answer=answer,
                    question_id=question.id,
                    respondent_id=respondent_id
                )
                db.session.add(response)
        
        db.session.commit()
        return redirect(url_for('survey.thank_you', id=survey.id))
    
    # GET ์š”์ฒญ ์ฒ˜๋ฆฌ (์„ค๋ฌธ ํผ ํ‘œ์‹œ)
    respondent_id = str(uuid.uuid4())  # ์ƒˆ ์‘๋‹ต์ž ID ์ƒ์„ฑ
    return render_template('survey/respond.html',
                          title=survey.title,
                          survey=survey,
                          questions=questions,
                          respondent_id=respondent_id)

@bp.route('/thank_you/<int:id>')
def thank_you(id):
    survey = Survey.query.get_or_404(id)
    return render_template('survey/thank_you.html', title='์‘๋‹ต ์™„๋ฃŒ', survey=survey)
    

์ด์ œ ์‘๋‹ต ํŽ˜์ด์ง€ ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/survey/respond.html:


{% extends "base.html" %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-8 offset-md-2">
        <div class="card">
            <div class="card-header">
                <h2 class="card-title">{{ survey.title }}</h2>
            </div>
            <div class="card-body">
                <p class="lead">{{ survey.description }}</p>
                <hr>
                
                <form action="" method="post" novalidate>
                    <input type="hidden" name="respondent_id" value="{{ respondent_id }}">
                    
                    {% for question in questions %}
                    <div class="mb-4 p-3 border rounded {% if question.required %}border-danger{% endif %}">
                        <h5>
                            {{ question.content }}
                            {% if question.required %}
                            <span class="text-danger">*</span>
                            {% endif %}
                        </h5>
                        
                        {% if question.type == 'text' %}
                        <!-- ์ฃผ๊ด€์‹ ์งˆ๋ฌธ -->
                        <div class="mb-3">
                            <textarea class="form-control" name="question_{{ question.id }}" rows="3" {% if question.required %}required{% endif %}></textarea>
                        </div>
                        
                        {% elif question.type == 'radio' %}
                        <!-- ๊ฐ๊ด€์‹ ๋‹จ์ผ ์„ ํƒ ์งˆ๋ฌธ -->
                        <div class="mb-3">
                            {% for option in question.options %}
                            <div class="form-check">
                                <input class="form-check-input" type="radio" name="question_{{ question.id }}" id="option_{{ option.id }}" value="{{ option.text }}" {% if question.required %}required{% endif %}>
                                <label class="form-check-label" for="option_{{ option.id }}">
                                    {{ option.text }}
                                </label>
                            </div>
                            {% endfor %}
                        </div>
                        
                        {% elif question.type == 'checkbox' %}
                        <!-- ๊ฐ๊ด€์‹ ๋‹ค์ค‘ ์„ ํƒ ์งˆ๋ฌธ -->
                        <div class="mb-3">
                            {% for option in question.options %}
                            <div class="form-check">
                                <input class="form-check-input" type="checkbox" name="question_{{ question.id }}" id="option_{{ option.id }}" value="{{ option.text }}">
                                <label class="form-check-label" for="option_{{ option.id }}">
                                    {{ option.text }}
                                </label>
                            </div>
                            {% endfor %}
                        </div>
                        
                        {% elif question.type == 'dropdown' %}
                        <!-- ๋“œ๋กญ๋‹ค์šด ์งˆ๋ฌธ -->
                        <div class="mb-3">
                            <select class="form-select" name="question_{{ question.id }}" {% if question.required %}required{% endif %}>
                                <option value="">-- ์„ ํƒํ•˜์„ธ์š” --</option>
                                {% for option in question.options %}
                                <option value="{{ option.text }}">{{ option.text }}</option>
                                {% endfor %}
                            </select>
                        </div>
                        {% endif %}
                    </div>
                    {% endfor %}
                    
                    <div class="d-grid gap-2">
                        <button type="submit" class="btn btn-primary btn-lg">์‘๋‹ต ์ œ์ถœํ•˜๊ธฐ</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}
    

๊ฐ์‚ฌ ํŽ˜์ด์ง€๋„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/survey/thank_you.html:


{% extends "base.html" %}

{% block content %}
<div class="row">
    <div class="col-md-8 offset-md-2 text-center">
        <div class="card">
            <div class="card-body">
                <h1 class="display-4 text-success">๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!</h1>
                <p class="lead">{{ survey.title }} ์„ค๋ฌธ์— ์‘๋‹ตํ•ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.</p>
                <hr>
                <p>์†Œ์ค‘ํ•œ ์˜๊ฒฌ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ œ์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</p>
                <div class="mt-4">
                    <a href="{{ url_for('main.index') }}" class="btn btn-primary">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</a>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}
    

์ด์ œ ์„ค๋ฌธ ์‘๋‹ต ๊ธฐ๋Šฅ์ด ์™„์„ฑ๋์–ด์š”! ๋‹ค์Œ์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ์š”.

์„ค๋ฌธ ๊ณต์œ  ๋งํฌ ๋˜๋Š” QR ์ฝ”๋“œ ์‘๋‹ต์ž ์ ‘์† ์„ค๋ฌธ ํŽ˜์ด์ง€ ๋กœ๋“œ ์‘๋‹ต ์ œ์ถœ ๋ฐ์ดํ„ฐ ์ €์žฅ ๊ฐ์‚ฌ ํŽ˜์ด์ง€ ์‘๋‹ต ์™„๋ฃŒ ๋ฉ”์‹œ์ง€ ๊ฒฐ๊ณผ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™” ์„ค๋ฌธ ๊ด€๋ฆฌ์ž ๊ฒฐ๊ณผ ํ™•์ธ ๋ฐ ๋‚ด๋ณด๋‚ด๊ธฐ ์„ค๋ฌธ ์‘๋‹ต ํ”„๋กœ์„ธ์Šค ํ๋ฆ„๋„

7. ๊ฒฐ๊ณผ ๋ถ„์„ ๋ฐ ์‹œ๊ฐํ™” ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ“ˆ

์ด์ œ ์ˆ˜์ง‘๋œ ์„ค๋ฌธ ๊ฒฐ๊ณผ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”! ์ด ๋ถ€๋ถ„์ด ์ง„์งœ ๊ฟ€์žผ ํŒŒํŠธ์˜ˆ์š”~ ๋ฐ์ดํ„ฐ๋ฅผ ์˜ˆ์˜๊ฒŒ ๋ณด์—ฌ์ฃผ๋ฉด ์‚ฌ์šฉ์ž๋“ค์ด ํ™˜ํ˜ธํ•  ๊ฑฐ์˜ˆ์š”! ใ…‹ใ…‹ใ…‹

7.1 ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ๊ตฌํ˜„ํ•˜๊ธฐ

app/survey/routes.py์— ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”:


from collections import Counter
import json

@bp.route('/results/<int:id>')
@login_required
def results(id):
    survey = Survey.query.get_or_404(id)
    if survey.author != current_user:
        flash('์ด ์„ค๋ฌธ์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('main.index'))
    
    # ์งˆ๋ฌธ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
    questions = survey.questions.order_by(Question.order).all()
    
    # ๊ฐ ์งˆ๋ฌธ๋ณ„ ์‘๋‹ต ๋ฐ์ดํ„ฐ ๋ถ„์„
    results_data = []
    
    for question in questions:
        question_data = {
            'id': question.id,
            'content': question.content,
            'type': question.type,
            'total_responses': question.responses.count()
        }
        
        if question.type == 'text':
            # ์ฃผ๊ด€์‹ ์‘๋‹ต์€ ํ…์ŠคํŠธ ๋ชฉ๋ก์œผ๋กœ ํ‘œ์‹œ
            question_data['responses'] = [r.answer for r in question.responses]
            
        elif question.type in ['radio', 'dropdown']:
            # ๋‹จ์ผ ์„ ํƒ ์‘๋‹ต์€ ๊ฐ ์˜ต์…˜๋ณ„ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ
            answers = [r.answer for r in question.responses]
            counts = Counter(answers)
            
            # ์ฐจํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
            labels = list(counts.keys())
            values = list(counts.values())
            
            question_data['chart_data'] = {
                'labels': labels,
                'values': values
            }
            
        elif question.type == 'checkbox':
            # ๋‹ค์ค‘ ์„ ํƒ ์‘๋‹ต์€ ์ฝค๋งˆ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐ ์˜ต์…˜๋ณ„ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ
            all_selections = []
            for response in question.responses:
                if response.answer:
                    selections = response.answer.split(',')
                    all_selections.extend(selections)
            
            counts = Counter(all_selections)
            
            # ์ฐจํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„
            labels = list(counts.keys())
            values = list(counts.values())
            
            question_data['chart_data'] = {
                'labels': labels,
                'values': values
            }
        
        results_data.append(question_data)
    
    # ์ด ์‘๋‹ต์ž ์ˆ˜ ๊ณ„์‚ฐ (์ค‘๋ณต ์ œ๊ฑฐ)
    respondent_ids = set()
    for question in questions:
        for response in question.responses:
            respondent_ids.add(response.respondent_id)
    
    total_respondents = len(respondent_ids)
    
    return render_template('survey/results.html',
                          title=f'{survey.title} - ๊ฒฐ๊ณผ',
                          survey=survey,
                          results_data=results_data,
                          total_respondents=total_respondents)
    

์ด์ œ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/survey/results.html:


{% extends "base.html" %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-8">
        <h2>์„ค๋ฌธ ๊ฒฐ๊ณผ <small class="text-muted">{{ survey.title }}</small></h2>
        <p>
            <a href="{{ url_for('survey.my_surveys') }}">โ† ๋‚ด ์„ค๋ฌธ ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</a>
        </p>
    </div>
    <div class="col-md-4 text-end">
        <a href="{{ url_for('survey.export_results', id=survey.id) }}" class="btn btn-success">๊ฒฐ๊ณผ ๋‚ด๋ณด๋‚ด๊ธฐ (CSV)</a>
    </div>
</div>

<div class="row mb-4">
    <div class="col-md-12">
        <div class="card">
            <div class="card-body">
                <div class="row">
                    <div class="col-md-4 text-center">
                        <h3 class="display-4">{{ total_respondents }}</h3>
                        <p class="text-muted">์ด ์‘๋‹ต์ž ์ˆ˜</p>
                    </div>
                    <div class="col-md-4 text-center">
                        <h3 class="display-4">{{ survey.questions.count() }}</h3>
                        <p class="text-muted">์ด ์งˆ๋ฌธ ์ˆ˜</p>
                    </div>
                    <div class="col-md-4 text-center">
                        <h3 class="display-4">{{ survey.created_at.strftime('%Y-%m-%d') }}</h3>
                        <p class="text-muted">์„ค๋ฌธ ์ƒ์„ฑ์ผ</p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

{% for data in results_data %}
<div class="row mb-4">
    <div class="col-md-12">
        <div class="card">
            <div class="card-header">
                <h4 class="card-title">{{ data.content }}</h4>
                <div class="text-muted">
                    {{ data.type }} ์œ ํ˜• | {{ data.total_responses }}๊ฐœ ์‘๋‹ต
                </div>
            </div>
            <div class="card-body">
                {% if data.type == 'text' %}
                <!-- ์ฃผ๊ด€์‹ ์‘๋‹ต ํ‘œ์‹œ -->
                <div class="table-responsive">
                    <table class="table table-striped">
                        <thead>
                            <tr>
                                <th>#</th>
                                <th>์‘๋‹ต ๋‚ด์šฉ</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for response in data.responses %}
                            <tr>
                                <td>{{ loop.index }}</td>
                                <td>{{ response }}</td>
                            </tr>
                            {% endfor %}
                        </tbody>
                    </table>
                </div>
                {% elif data.type in ['radio', 'dropdown', 'checkbox'] %}
                <!-- ๊ฐ๊ด€์‹ ์‘๋‹ต ์ฐจํŠธ ํ‘œ์‹œ -->
                <div class="row">
                    <div class="col-md-8">
                        <canvas id="chart_{{ data.id }}" width="400" height="200"></canvas>
                    </div>
                    <div class="col-md-4">
                        <table class="table table-sm">
                            <thead>
                                <tr>
                                    <th>์˜ต์…˜</th>
                                    <th>์‘๋‹ต ์ˆ˜</th>
                                    <th>๋น„์œจ</th>
                                </tr>
                            </thead>
                            <tbody>
                                {% for i in range(data.chart_data.labels|length) %}
                                <tr>
                                    <td>{{ data.chart_data.labels[i] }}</td>
                                    <td>{{ data.chart_data.values[i] }}</td>
                                    <td>
                                        {% if data.total_responses > 0 %}
                                        {{ (data.chart_data.values[i] / data.total_responses * 100)|round(1) }}%
                                        {% else %}
                                        0%
                                        {% endif %}
                                    </td>
                                </tr>
                                {% endfor %}
                            </tbody>
                        </table>
                    </div>
                </div>
                {% endif %}
            </div>
        </div>
    </div>
</div>
{% endfor %}
{% endblock %}

{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
    {% for data in results_data %}
    {% if data.type in ['radio', 'dropdown', 'checkbox'] and data.chart_data %}
    // ์ฐจํŠธ ์ƒ์„ฑ
    var ctx = document.getElementById('chart_{{ data.id }}').getContext('2d');
    var chart = new Chart(ctx, {
        type: '{{ 'pie' if data.type in ['radio', 'dropdown'] else 'bar' }}',
        data: {
            labels: {{ data.chart_data.labels|tojson }},
            datasets: [{
                label: '์‘๋‹ต ์ˆ˜',
                data: {{ data.chart_data.values|tojson }},
                backgroundColor: [
                    'rgba(255, 99, 132, 0.7)',
                    'rgba(54, 162, 235, 0.7)',
                    'rgba(255, 206, 86, 0.7)',
                    'rgba(75, 192, 192, 0.7)',
                    'rgba(153, 102, 255, 0.7)',
                    'rgba(255, 159, 64, 0.7)',
                    'rgba(199, 199, 199, 0.7)',
                    'rgba(83, 102, 255, 0.7)',
                    'rgba(40, 159, 64, 0.7)',
                    'rgba(210, 199, 199, 0.7)'
                ],
                borderColor: [
                    'rgba(255, 99, 132, 1)',
                    'rgba(54, 162, 235, 1)',
                    'rgba(255, 206, 86, 1)',
                    'rgba(75, 192, 192, 1)',
                    'rgba(153, 102, 255, 1)',
                    'rgba(255, 159, 64, 1)',
                    'rgba(199, 199, 199, 1)',
                    'rgba(83, 102, 255, 1)',
                    'rgba(40, 159, 64, 1)',
                    'rgba(210, 199, 199, 1)'
                ],
                borderWidth: 1
            }]
        },
        options: {
            responsive: true,
            {% if data.type == 'checkbox' %}
            scales: {
                y: {
                    beginAtZero: true
                }
            },
            {% endif %}
            plugins: {
                legend: {
                    position: 'right',
                },
                title: {
                    display: true,
                    text: '{{ data.content }}'
                }
            }
        }
    });
    {% endif %}
    {% endfor %}
});
</script>
{% endblock %}
    

7.2 ๊ฒฐ๊ณผ ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

CSV ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ๋‚ด๋ณด๋‚ด๋Š” ๊ธฐ๋Šฅ๋„ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”. app/survey/routes.py์— ๋‹ค์Œ ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


import csv
from io import StringIO
from flask import make_response

@bp.route('/export_results/<int:id>')
@login_required
def export_results(id):
    survey = Survey.query.get_or_404(id)
    if survey.author != current_user:
        flash('์ด ์„ค๋ฌธ์˜ ๊ฒฐ๊ณผ๋ฅผ ๋‚ด๋ณด๋‚ผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('main.index'))
    
    # CSV ํŒŒ์ผ ์ƒ์„ฑ
    output = StringIO()
    writer = csv.writer(output)
    
    # ํ—ค๋” ํ–‰ ์ž‘์„ฑ
    questions = survey.questions.order_by(Question.order).all()
    header = ['์‘๋‹ต์ž ID', '์‘๋‹ต ์‹œ๊ฐ„']
    for question in questions:
        header.append(f'Q{question.id}: {question.content}')
    
    writer.writerow(header)
    
    # ์‘๋‹ต์ž ID ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
    respondent_ids = set()
    for question in questions:
        for response in question.responses:
            respondent_ids.add(response.respondent_id)
    
    # ๊ฐ ์‘๋‹ต์ž๋ณ„ ๋ฐ์ดํ„ฐ ์ž‘์„ฑ
    for respondent_id in respondent_ids:
        row = [respondent_id]
        
        # ์‘๋‹ต ์‹œ๊ฐ„ (ํ•ด๋‹น ์‘๋‹ต์ž์˜ ์ฒซ ๋ฒˆ์งธ ์‘๋‹ต ์‹œ๊ฐ„)
        first_response = Response.query.filter_by(respondent_id=respondent_id).order_by(Response.created_at).first()
        row.append(first_response.created_at.strftime('%Y-%m-%d %H:%M:%S') if first_response else '')
        
        # ๊ฐ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ์‘๋‹ต
        for question in questions:
            response = Response.query.filter_by(question_id=question.id, respondent_id=respondent_id).first()
            row.append(response.answer if response else '')
        
        writer.writerow(row)
    
    # CSV ํŒŒ์ผ ์‘๋‹ต ์ƒ์„ฑ
    output.seek(0)
    response = make_response(output.getvalue())
    response.headers['Content-Disposition'] = f'attachment; filename=survey_{survey.id}_results.csv'
    response.headers['Content-type'] = 'text/csv'
    
    return response
    

์ด์ œ ์„ค๋ฌธ ๊ฒฐ๊ณผ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์™„์„ฑ๋์–ด์š”! ๋‹ค์Œ์œผ๋กœ ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๋” ๊ฐœ์„ ํ•ด๋ณผ๊ฒŒ์š”.

์˜ต์…˜ A (30%) ์˜ต์…˜ B (25%) ์˜ต์…˜ C (20%) ์˜ต์…˜ D (15%) ์˜ต์…˜ E (10%) ์˜ต์…˜ A ์˜ต์…˜ B ์˜ต์…˜ C ์˜ต์…˜ D ์˜ต์…˜ E 0 25 50 75 100 125 150 ์„ค๋ฌธ ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”

8. ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ ์ถ”๊ฐ€ํ•˜๊ธฐ ๐Ÿ”

์ง€๊ธˆ๊นŒ์ง€ ๊ธฐ๋ณธ์ ์ธ ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ–ˆ์ง€๋งŒ, ์ข€ ๋” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”! ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •, ํ”„๋กœํ•„ ๊ด€๋ฆฌ ๋“ฑ ์‹ค์ œ ์„œ๋น„์Šค์—์„œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ๊ตฌํ˜„ํ•ด๋ด…์‹œ๋‹ค.

8.1 ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

์‚ฌ์šฉ์ž๊ฐ€ ์ž์‹ ์˜ ํ”„๋กœํ•„์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”. app/auth/forms.py์— ํ”„๋กœํ•„ ์ˆ˜์ • ํผ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


class ProfileForm(FlaskForm):
    username = StringField('์‚ฌ์šฉ์ž ์ด๋ฆ„', validators=[DataRequired()])
    email = StringField('์ด๋ฉ”์ผ', validators=[DataRequired(), Email()])
    current_password = PasswordField('ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ')
    new_password = PasswordField('์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ')
    new_password2 = PasswordField(
        '์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ', validators=[EqualTo('new_password')])
    submit = SubmitField('ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ')
    
    def __init__(self, original_username, original_email, *args, **kwargs):
        super(ProfileForm, self).__init__(*args, **kwargs)
        self.original_username = original_username
        self.original_email = original_email
    
    def validate_username(self, username):
        if username.data != self.original_username:
            user = User.query.filter_by(username=username.data).first()
            if user is not None:
                raise ValidationError('์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์‚ฌ์šฉ์ž ์ด๋ฆ„์ž…๋‹ˆ๋‹ค.')
    
    def validate_email(self, email):
        if email.data != self.original_email:
            user = User.query.filter_by(email=email.data).first()
            if user is not None:
                raise ValidationError('์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.')
    

์ด์ œ app/auth/routes.py์— ํ”„๋กœํ•„ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


@bp.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
    form = ProfileForm(current_user.username, current_user.email)
    if form.validate_on_submit():
        # ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ์„ ์›ํ•˜๋Š” ๊ฒฝ์šฐ
        if form.current_password.data:
            if not current_user.check_password(form.current_password.data):
                flash('ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.')
                return redirect(url_for('auth.profile'))
            
            if form.new_password.data:
                current_user.set_password(form.new_password.data)
                flash('๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
        
        # ์‚ฌ์šฉ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ
        current_user.username = form.username.data
        current_user.email = form.email.data
        db.session.commit()
        flash('ํ”„๋กœํ•„์ด ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('auth.profile'))
    elif request.method == 'GET':
        form.username.data = current_user.username
        form.email.data = current_user.email
    
    return render_template('auth/profile.html', title='ํ”„๋กœํ•„ ๊ด€๋ฆฌ', form=form)
    

์ด์ œ ํ”„๋กœํ•„ ๊ด€๋ฆฌ ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/auth/profile.html:


{% extends "base.html" %}

{% block content %}
<div class="row">
    <div class="col-md-8 offset-md-2">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title">ํ”„๋กœํ•„ ๊ด€๋ฆฌ</h3>
            </div>
            <div class="card-body">
                <form action="" method="post" novalidate>
                    {{ form.hidden_tag() }}
                    <div class="mb-3">
                        {{ form.username.label(class="form-label") }}
                        {{ form.username(class="form-control") }}
                        {% for error in form.username.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ form.email.label(class="form-label") }}
                        {{ form.email(class="form-control") }}
                        {% for error in form.email.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    
                    <hr>
                    <h4>๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ</h4>
                    <p class="text-muted">๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด ์•„๋ž˜ ํ•ญ๋ชฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”. ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์œผ๋ ค๋ฉด ๋น„์›Œ๋‘์„ธ์š”.</p>
                    
                    <div class="mb-3">
                        {{ form.current_password.label(class="form-label") }}
                        {{ form.current_password(class="form-control") }}
                        {% for error in form.current_password.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ form.new_password.label(class="form-label") }}
                        {{ form.new_password(class="form-control") }}
                        {% for error in form.new_password.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ form.new_password2.label(class="form-label") }}
                        {{ form.new_password2(class="form-control") }}
                        {% for error in form.new_password2.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    
                    <div class="mb-3">
                        {{ form.submit(class="btn btn-primary") }}
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}
    

8.2 ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žŠ์–ด๋ฒ„๋ฆฐ ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ์žฌ์„ค์ • ๊ธฐ๋Šฅ๋„ ์ถ”๊ฐ€ํ•ด๋ณผ๊ฒŒ์š”. ๋จผ์ € app/auth/forms.py์— ํผ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


class ResetPasswordRequestForm(FlaskForm):
    email = StringField('์ด๋ฉ”์ผ', validators=[DataRequired(), Email()])
    submit = SubmitField('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์š”์ฒญ')

class ResetPasswordForm(FlaskForm):
    password = PasswordField('์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ', validators=[DataRequired()])
    password2 = PasswordField(
        '๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •')
    

์ด๋ฉ”์ผ ์ „์†ก ๊ธฐ๋Šฅ์„ ์œ„ํ•ด app/email.py ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


from flask import render_template, current_app
from flask_mail import Message
from app import mail
from threading import Thread

def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    
    # ๋น„๋™๊ธฐ๋กœ ์ด๋ฉ”์ผ ์ „์†ก (๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…)
    Thread(target=send_async_email,
          args=(current_app._get_current_object(), msg)).start()

def send_password_reset_email(user):
    token = user.get_reset_password_token()
    send_email(
        '[์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ] ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •',
        sender=current_app.config['MAIL_DEFAULT_SENDER'],
        recipients=[user.email],
        text_body=render_template('email/reset_password.txt',
                                 user=user, token=token),
        html_body=render_template('email/reset_password.html',
                                 user=user, token=token)
    )
    

app/models.py์— ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ† ํฐ ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


from time import time
import jwt
from flask import current_app

class User(UserMixin, db.Model):
    # ๊ธฐ์กด ์ฝ”๋“œ๋Š” ๊ทธ๋Œ€๋กœ ๋‘๊ณ  ์•„๋ž˜ ๋ฉ”์„œ๋“œ๋งŒ ์ถ”๊ฐ€
    
    def get_reset_password_token(self, expires_in=600):
        return jwt.encode(
            {'reset_password': self.id, 'exp': time() + expires_in},
            current_app.config['SECRET_KEY'],
            algorithm='HS256'
        )
    
    @staticmethod
    def verify_reset_password_token(token):
        try:
            id = jwt.decode(
                token,
                current_app.config['SECRET_KEY'],
                algorithms=['HS256']
            )['reset_password']
        except:
            return None
        return User.query.get(id)
    

์ด์ œ app/auth/routes.py์— ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋ผ์šฐํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


from app.email import send_password_reset_email

@bp.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    form = ResetPasswordRequestForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user:
            send_password_reset_email(user)
        flash('๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์•ˆ๋‚ด ์ด๋ฉ”์ผ์„ ๋ฐœ์†กํ–ˆ์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('auth.login'))
    return render_template('auth/reset_password_request.html',
                          title='๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •', form=form)

@bp.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    user = User.verify_reset_password_token(token)
    if not user:
        flash('์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.')
        return redirect(url_for('main.index'))
    form = ResetPasswordForm()
    if form.validate_on_submit():
        user.set_password(form.password.data)
        db.session.commit()
        flash('๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์žฌ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
        return redirect(url_for('auth.login'))
    return render_template('auth/reset_password.html', form=form)
    

์ด์ œ ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. app/templates/auth/reset_password_request.html:


{% extends "base.html" %}

{% block content %}
<div class="row">
    <div class="col-md-6 offset-md-3">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title">๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์š”์ฒญ</h3>
            </div>
            <div class="card-body">
                <form action="" method="post" novalidate>
                    {{ form.hidden_tag() }}
                    <div class="mb-3">
                        {{ form.email.label(class="form-label") }}
                        {{ form.email(class="form-control") }}
                        {% for error in form.email.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ form.submit(class="btn btn-primary") }}
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}
    

app/templates/auth/reset_password.html:


{% extends "base.html" %}

{% block content %}
<div class="row">
    <div class="col-md-6 offset-md-3">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title">๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •</h3>
            </div>
            <div class="card-body">
                <form action="" method="post" novalidate>
                    {{ form.hidden_tag() }}
                    <div class="mb-3">
                        {{ form.password.label(class="form-label") }}
                        {{ form.password(class="form-control") }}
                        {% for error in form.password.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ form.password2.label(class="form-label") }}
                        {{ form.password2(class="form-control") }}
                        {% for error in form.password2.errors %}
                        <span class="text-danger">{{ error }}</span>
                        {% endfor %}
                    </div>
                    <div class="mb-3">
                        {{ form.submit(class="btn btn-primary") }}
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}
    

์ด๋ฉ”์ผ ํ…œํ”Œ๋ฆฟ๋„ ๋งŒ๋“ค์–ด์•ผ ํ•ด์š”. app/templates/email/reset_password.html:


<p>์•ˆ๋…•ํ•˜์„ธ์š” {{ user.username }}๋‹˜,</p>
<p>
    ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •์„ ์š”์ฒญํ•˜์…จ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ ๋งํฌ๋ฅผ ํด๋ฆญํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žฌ์„ค์ •ํ•˜์„ธ์š”:
</p>
<p>
    <a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
        ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •ํ•˜๊ธฐ
    </a>
</p>
<p>
    ์œ„ ๋งํฌ๋Š” 10๋ถ„ ๋™์•ˆ๋งŒ ์œ ํšจํ•ฉ๋‹ˆ๋‹ค.
</p>
<p>
    ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •์„ ์š”์ฒญํ•˜์ง€ ์•Š์œผ์…จ๋‹ค๋ฉด ์ด ์ด๋ฉ”์ผ์„ ๋ฌด์‹œํ•˜์…”๋„ ๋ฉ๋‹ˆ๋‹ค.
</p>
<p>์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ ํŒ€</p>
    

์ด์ œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์— ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋งํฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”. app/templates/auth/login.html์˜ ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•ด์ฃผ์„ธ์š”:


<p>๊ณ„์ •์ด ์—†์œผ์‹ ๊ฐ€์š”? <a href="{{ url_for('auth.register') }}">ํšŒ์›๊ฐ€์ž…</a></p>
<p>๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žŠ์œผ์…จ๋‚˜์š”? <a href="{{ url_for('auth.reset_password_request') }}">๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •</a></p>
    

์ด์ œ ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ์ด ๋” ์™„๋ฒฝํ•ด์กŒ์–ด์š”! ๋‹ค์Œ์œผ๋กœ ๋ฐฐํฌ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”.

9. ๋ฐฐํฌ ๋ฐ ์šด์˜ํ•˜๊ธฐ ๐Ÿš€

์ด์ œ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ๋ฉ‹์ง„ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์„ ์‹ค์ œ๋กœ ๋ฐฐํฌํ•ด๋ณผ ์ฐจ๋ก€์˜ˆ์š”! ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ๋Œ๋ฆฌ๋Š” ๊ฑด ์žฌ๋ฏธ์—†์ž–์•„์š”? ใ…‹ใ…‹ใ…‹ ์‹ค์ œ ์„œ๋ฒ„์— ์˜ฌ๋ ค์„œ ์นœ๊ตฌ๋“ค์—๊ฒŒ ์ž๋ž‘ํ•ด๋ด…์‹œ๋‹ค!

9.1 ๋ฐฐํฌ ์ค€๋น„ํ•˜๊ธฐ

๋ฐฐํฌํ•˜๊ธฐ ์ „์— ๋ช‡ ๊ฐ€์ง€ ์ค€๋น„๊ฐ€ ํ•„์š”ํ•ด์š”:

  1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •ํ•˜๊ธฐ
  2. ์˜์กด์„ฑ ๋ชฉ๋ก ๋งŒ๋“ค๊ธฐ
  3. ํ”„๋กœ๋•์…˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •ํ•˜๊ธฐ
  4. WSGI ์„œ๋ฒ„ ์„ค์ •ํ•˜๊ธฐ

๋จผ์ € requirements.txt ํŒŒ์ผ์„ ์—…๋ฐ์ดํŠธํ•ด๋ณผ๊ฒŒ์š”:


# ๊ฐ€์ƒํ™˜๊ฒฝ์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด ์‹คํ–‰
pip freeze > requirements.txt
    

์ด์ œ .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•ด๋ณผ๊ฒŒ์š”:


SECRET_KEY=your-super-secret-key-here
DATABASE_URL=sqlite:///app.db
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=1
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-email-password
MAIL_DEFAULT_SENDER=your-email@gmail.com
    

์‹ค์ œ ๋ฐฐํฌ ์‹œ์—๋Š” SQLite ๋Œ€์‹  PostgreSQL ๊ฐ™์€ ํ”„๋กœ๋•์…˜์šฉ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š”.

9.2 Heroku์— ๋ฐฐํฌํ•˜๊ธฐ

Heroku๋Š” ์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋Š” ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ์ด์—์š”. ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ผ ๋ฐฐํฌํ•ด๋ณผ๊ฒŒ์š”:

  1. Heroku ๊ณ„์ • ๋งŒ๋“ค๊ธฐ (https://signup.heroku.com/)
  2. Heroku CLI ์„ค์น˜ํ•˜๊ธฐ (https://devcenter.heroku.com/articles/heroku-cli)
  3. ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— Procfile ๋งŒ๋“ค๊ธฐ

Procfile ๋‚ด์šฉ:


web: gunicorn run:app
    

์ด์ œ requirements.txt์— gunicorn์„ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


pip install gunicorn
pip freeze > requirements.txt
    

์ด์ œ Heroku์— ๋ฐฐํฌํ•ด๋ณผ๊ฒŒ์š”:


# Heroku ๋กœ๊ทธ์ธ
heroku login

# ์•ฑ ์ƒ์„ฑ
heroku create your-survey-app-name

# Git ์ €์žฅ์†Œ ์ดˆ๊ธฐํ™” (์•„์ง ์•ˆ ํ–ˆ๋‹ค๋ฉด)
git init
git add .
git commit -m "Initial commit"

# Heroku์— ํ‘ธ์‹œ
git push heroku master

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
heroku run flask db upgrade

# ์•ฑ ์—ด๊ธฐ
heroku open
    

ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”:


heroku config:set SECRET_KEY=your-super-secret-key-here
heroku config:set MAIL_SERVER=smtp.gmail.com
heroku config:set MAIL_PORT=587
heroku config:set MAIL_USE_TLS=1
heroku config:set MAIL_USERNAME=your-email@gmail.com
heroku config:set MAIL_PASSWORD=your-email-password
heroku config:set MAIL_DEFAULT_SENDER=your-email@gmail.com
    

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์ด ์ธํ„ฐ๋„ท์— ๊ณต๊ฐœ๋˜์—ˆ์–ด์š”! ๐ŸŽ‰

9.3 PythonAnywhere์— ๋ฐฐํฌํ•˜๊ธฐ

Heroku ์™ธ์—๋„ PythonAnywhere๋ผ๋Š” ํ”Œ๋žซํผ๋„ ์žˆ์–ด์š”. ์ด ํ”Œ๋žซํผ์€ Python ์•ฑ ๋ฐฐํฌ์— ํŠนํ™”๋˜์–ด ์žˆ๊ณ , ๋ฌด๋ฃŒ ํ‹ฐ์–ด๋„ ์ œ๊ณตํ•ด์š”.

  1. PythonAnywhere ๊ณ„์ • ๋งŒ๋“ค๊ธฐ (https://www.pythonanywhere.com/)
  2. Bash ์ฝ˜์†” ์—ด๊ธฐ
  3. ๊ฐ€์ƒํ™˜๊ฒฝ ์„ค์ • ๋ฐ ์ฝ”๋“œ ๊ฐ€์ ธ์˜ค๊ธฐ

PythonAnywhere Bash ์ฝ˜์†”์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”:


# ์ฝ”๋“œ ํด๋ก  (GitHub์— ์˜ฌ๋ ค๋†จ๋‹ค๋ฉด)
git clone https://github.com/yourusername/your-survey-app.git

# ๋˜๋Š” ZIP ํŒŒ์ผ๋กœ ์—…๋กœ๋“œ ํ›„ ์••์ถ• ํ•ด์ œ
unzip your-survey-app.zip

# ๋””๋ ‰ํ† ๋ฆฌ ์ด๋™
cd your-survey-app

# ๊ฐ€์ƒํ™˜๊ฒฝ ์ƒ์„ฑ ๋ฐ ํ™œ์„ฑํ™”
python -m venv venv
source venv/bin/activate

# ํŒจํ‚ค์ง€ ์„ค์น˜
pip install -r requirements.txt
    

์ด์ œ PythonAnywhere ์›น ์ธํ„ฐํŽ˜์ด์Šค์—์„œ 'Web' ํƒญ์œผ๋กœ ์ด๋™ํ•ด ์ƒˆ ์›น ์•ฑ์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”:

  1. 'Add a new web app' ํด๋ฆญ
  2. 'Manual configuration' ์„ ํƒ
  3. Python ๋ฒ„์ „ ์„ ํƒ (3.8 ์ด์ƒ)
  4. ๊ฐ€์ƒํ™˜๊ฒฝ ๊ฒฝ๋กœ ์ž…๋ ฅ: /home/yourusername/your-survey-app/venv
  5. WSGI ํŒŒ์ผ ์ˆ˜์ • (์ œ๊ณต๋œ ๋งํฌ ํด๋ฆญ)

WSGI ํŒŒ์ผ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•ด์ฃผ์„ธ์š”:


import sys
import os

path = '/home/yourusername/your-survey-app'
if path not in sys.path:
    sys.path.append(path)

from dotenv import load_dotenv
load_dotenv(os.path.join(path, '.env'))

from run import app as application
    

ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋Š” .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ์„ค์ •ํ•˜๊ฑฐ๋‚˜, PythonAnywhere์˜ 'Web' ํƒญ์—์„œ 'Environment variables' ์„น์…˜์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด์š”.

๋งˆ์ง€๋ง‰์œผ๋กœ 'Reload' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด ์•ฑ์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

9.4 ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ ๋ฐ HTTPS ์„ค์ •

์‹ค์ œ ์„œ๋น„์Šค๋กœ ์šด์˜ํ•˜๋ ค๋ฉด ์ปค์Šคํ…€ ๋„๋ฉ”์ธ๊ณผ HTTPS๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์•„์š”:

  1. ๋„๋ฉ”์ธ ๊ตฌ๋งค (Namecheap, GoDaddy ๋“ฑ)
  2. DNS ์„ค์ • (ํ˜ธ์ŠคํŒ… ์—…์ฒด ์ง€์นจ ์ฐธ์กฐ)
  3. SSL ์ธ์ฆ์„œ ์„ค์ • (Let's Encrypt ์ถ”์ฒœ)

Heroku์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ปค์Šคํ…€ ๋„๋ฉ”์ธ์„ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด์š”:


heroku domains:add www.yourdomain.com
    

๊ทธ๋Ÿฐ ๋‹ค์Œ ๋„๋ฉ”์ธ ์ œ๊ณต์—…์ฒด์˜ DNS ์„ค์ •์—์„œ CNAME ๋ ˆ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”:


www.yourdomain.com -> your-app-name.herokuapp.com
    

Heroku์˜ ์œ ๋ฃŒ ํ”Œ๋žœ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ SSL ์ธ์ฆ์„œ๋ฅผ ๊ด€๋ฆฌํ•ด์ค˜์š”:


heroku certs:auto:enable
    

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์ด ์ „๋ฌธ์ ์ธ ๋„๋ฉ”์ธ๊ณผ ๋ณด์•ˆ ์—ฐ๊ฒฐ๋กœ ์šด์˜๋˜๊ณ  ์žˆ์–ด์š”! ๐Ÿ‘

10. ํ™•์žฅ ๋ฐ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์•„์ด๋””์–ด ๐Ÿ’ก

๊ธฐ๋ณธ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์„ ์™„์„ฑํ–ˆ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ ๋” ๋ฐœ์ „์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ์•„์ด๋””์–ด๊ฐ€ ๋งŽ์•„์š”! ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ ๋…ํŠนํ•œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”.

10.1 ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ์•„์ด๋””์–ด

๋‹ค์Œ์€ ์—ฌ๋Ÿฌ๋ถ„์˜ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์„ ๋” ๋ฉ‹์ง€๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ์•„์ด๋””์–ด๋“ค์ด์—์š”:

  1. ํ…Œ๋งˆ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•: ์‚ฌ์šฉ์ž๊ฐ€ ์„ค๋ฌธ์˜ ์ƒ‰์ƒ, ํฐํŠธ ๋“ฑ์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๋ณด์„ธ์š”
  2. ๋ถ„๊ธฐ ๋กœ์ง: ์ด์ „ ์งˆ๋ฌธ์˜ ์‘๋‹ต์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์งˆ๋ฌธ์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ
  3. ํŒŒ์ผ ์—…๋กœ๋“œ: ์‘๋‹ต์ž๊ฐ€ ์ด๋ฏธ์ง€๋‚˜ ๋ฌธ์„œ๋ฅผ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” ์งˆ๋ฌธ ์œ ํ˜• ์ถ”๊ฐ€
  4. ์†Œ์…œ ๋กœ๊ทธ์ธ: Google, Facebook ๋“ฑ์œผ๋กœ ๋กœ๊ทธ์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ
  5. ์‘๋‹ต ์ œํ•œ: IP ์ฃผ์†Œ๋‚˜ ์ด๋ฉ”์ผ๋กœ ์ค‘๋ณต ์‘๋‹ต ๋ฐฉ์ง€
  6. ์„ค๋ฌธ ํ…œํ”Œ๋ฆฟ: ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ์„ค๋ฌธ ์œ ํ˜•์˜ ํ…œํ”Œ๋ฆฟ ์ œ๊ณต
  7. ๊ณ ๊ธ‰ ๋ถ„์„: ์‘๋‹ต ๋ฐ์ดํ„ฐ์˜ ์ƒ๊ด€๊ด€๊ณ„ ๋ถ„์„, ์„ธ๊ทธ๋จผํŠธ๋ณ„ ํ•„ํ„ฐ๋ง ๋“ฑ
  8. ์•Œ๋ฆผ ์‹œ์Šคํ…œ: ์ƒˆ ์‘๋‹ต์ด ๋“ค์–ด์˜ค๋ฉด ์ด๋ฉ”์ผ์ด๋‚˜ ํ‘ธ์‹œ ์•Œ๋ฆผ ์ „์†ก
  9. ํ˜‘์—… ๊ธฐ๋Šฅ: ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ํ•จ๊ป˜ ์„ค๋ฌธ์„ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ
  10. API ์ œ๊ณต: ์™ธ๋ถ€ ์„œ๋น„์Šค์™€ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ๋Š” API ๊ฐœ๋ฐœ

์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์—ฌ๋Ÿฌ๋ถ„์˜ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์ด ์ƒ์šฉ ์„œ๋น„์Šค์™€ ๊ฒฌ์ค„ ๋งŒํ•œ ์ˆ˜์ค€์ด ๋  ๊ฑฐ์˜ˆ์š”! ๊ทธ๋ฆฌ๊ณ  ์ด๋Ÿฐ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์€ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ Python ์›น ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ํŒ๋งคํ•  ๋•Œ ์ข‹์€ ํฌํŠธํด๋ฆฌ์˜ค๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด์š”.

10.2 ์ฝ”๋“œ ์ตœ์ ํ™” ๋ฐ ๋ฆฌํŒฉํ† ๋ง

์‹ค์ œ ์„œ๋น„์Šค๋กœ ์šด์˜ํ•˜๋ ค๋ฉด ์ฝ”๋“œ ์ตœ์ ํ™”๋„ ์ค‘์š”ํ•ด์š”:

  1. ์บ์‹ฑ ๋„์ž…: Redis๋‚˜ Memcached๋ฅผ ์‚ฌ์šฉํ•ด ์ž์ฃผ ์ ‘๊ทผํ•˜๋Š” ๋ฐ์ดํ„ฐ ์บ์‹ฑ
  2. ๋น„๋™๊ธฐ ์ž‘์—…: Celery ๊ฐ™์€ ๋„๊ตฌ๋กœ ์ด๋ฉ”์ผ ์ „์†ก ๋“ฑ์˜ ์ž‘์—…์„ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ
  3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ธ๋ฑ์‹ฑ: ์ž์ฃผ ์กฐํšŒํ•˜๋Š” ํ•„๋“œ์— ์ธ๋ฑ์Šค ์ถ”๊ฐ€
  4. ํŽ˜์ด์ง€๋„ค์ด์…˜: ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ  ๋กœ๋”ฉ
  5. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ: ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์™€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋กœ ์•ˆ์ •์„ฑ ํ™•๋ณด

์ด๋Ÿฐ ์ตœ์ ํ™”๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ๋” ์ค‘์š”ํ•ด์ ธ์š”!

10.3 ์ˆ˜์ตํ™” ๋ฐฉ์•ˆ

์—ฌ๋Ÿฌ๋ถ„์˜ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์œผ๋กœ ์ˆ˜์ต์„ ์ฐฝ์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์–ด์š”:

  1. ํ”„๋ฆฌ๋ฏธ์—„ ํ”Œ๋žœ: ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์€ ๋ฌด๋ฃŒ, ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์€ ์œ ๋ฃŒ๋กœ ์ œ๊ณต
  2. ์‘๋‹ต ์ˆ˜ ์ œํ•œ: ๋ฌด๋ฃŒ ํ”Œ๋žœ์€ ์›” 100๊ฐœ ์‘๋‹ต๊นŒ์ง€, ๊ทธ ์ด์ƒ์€ ์œ ๋ฃŒ
  3. ํ™”์ดํŠธ ๋ผ๋ฒจ: ๊ธฐ์—…์ด ์ž์‚ฌ ๋ธŒ๋žœ๋“œ๋กœ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ œ๊ณต
  4. ์„ค๋ฌธ ํ…œํ”Œ๋ฆฟ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค: ์ „๋ฌธ๊ฐ€๊ฐ€ ๋งŒ๋“  ํ…œํ”Œ๋ฆฟ ํŒ๋งค
  5. API ์‚ฌ์šฉ๋ฃŒ: ์™ธ๋ถ€ ์„œ๋น„์Šค ์—ฐ๋™์„ ์œ„ํ•œ API ์‚ฌ์šฉ๋ฃŒ ๋ถ€๊ณผ

์ด๋Ÿฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ์„ ์ ์šฉํ•˜๋ฉด ์ทจ๋ฏธ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹ค์ œ ์ˆ˜์ต์›์œผ๋กœ ๋ฐœ์ „์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด์š”!

์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ ํ…Œ๋งˆ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๋ถ„๊ธฐ ๋กœ์ง ํŒŒ์ผ ์—…๋กœ๋“œ ์†Œ์…œ ๋กœ๊ทธ์ธ ์‘๋‹ต ์ œํ•œ ์„ค๋ฌธ ํ…œํ”Œ๋ฆฟ ๊ณ ๊ธ‰ ๋ถ„์„ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๊ธฐ๋Šฅ ์•„์ด๋””์–ด

10.4 ๋งˆ๋ฌด๋ฆฌ

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์€ Python๊ณผ Flask๋ฅผ ์‚ฌ์šฉํ•ด ์™„์ „ํ•œ ์„ค๋ฌธ์กฐ์‚ฌ ์‹œ์Šคํ…œ์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์› ์–ด์š”! ๐ŸŽ‰

์ด ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ์ˆ ์„ ์ตํ˜”์–ด์š”:

  1. Flask ์›น ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉ๋ฒ•
  2. SQLAlchemy๋ฅผ ์ด์šฉํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชจ๋ธ๋ง
  3. ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌํ˜„
  4. ๋™์  ํผ ์ฒ˜๋ฆฌ
  5. ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”
  6. ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ

์ด ๋ชจ๋“  ๊ธฐ์ˆ ์€ ๋‹ค์–‘ํ•œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ท€์ค‘ํ•œ ์ž์‚ฐ์ด์—์š”! ์ด์ œ ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ ์•„์ด๋””์–ด๋ฅผ ์ถ”๊ฐ€ํ•ด ๋” ๋ฉ‹์ง„ ์„œ๋น„์Šค๋กœ ๋ฐœ์ „์‹œ์ผœ๋ณด์„ธ์š”.

Python ์›น ๊ฐœ๋ฐœ์€ ์ •๋ง ์žฌ๋ฏธ์žˆ๊ณ  ๋ฌด๊ถ๋ฌด์ง„ํ•œ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ๋ถ„์•ผ์˜ˆ์š”. ์ด ํŠœํ† ๋ฆฌ์–ผ์ด ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ฐœ๋ฐœ ์—ฌ์ •์— ๋„์›€์ด ๋˜์—ˆ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค! ๐Ÿ˜Š

ํ˜น์‹œ ์ด๋Ÿฐ ๊ฐœ๋ฐœ ์Šคํ‚ฌ์„ ๋” ๋ฐœ์ „์‹œํ‚ค๊ณ  ์‹ถ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋‹ค์–‘ํ•œ Python ์›น ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”. ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค์˜ ๋…ธํ•˜์šฐ๋ฅผ ๋ฐฐ์šฐ๊ณ , ๋‚˜์ค‘์—๋Š” ์—ฌ๋Ÿฌ๋ถ„๋„ ์ž์‹ ๋งŒ์˜ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์˜ˆ์š”!

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.