์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ’ผ AI๋กœ ํˆฌ์ž๋ฆฌํฌํŠธ ๋ถ„์„ํ•˜๊ณ  ํ•ต์‹ฌ๋งŒ ์™์™ ๋ฝ‘์•„๋‚ด๊ธฐ

๐Ÿ’ผ AI๋กœ ํˆฌ์ž๋ฆฌํฌํŠธ ๋ถ„์„ํ•˜๊ณ  ํ•ต์‹ฌ๋งŒ ์™์™ ๋ฝ‘์•„๋‚ด๊ธฐ

๋ณต์žกํ•œ ํˆฌ์ž ๋ฆฌํฌํŠธ, ์ด์ œ AI ์—์ด์ „ํŠธ๊ฐ€ ์•Œ์•„์„œ ์ •๋ฆฌํ•ด๋“œ๋ฆฝ๋‹ˆ๋‹ค! ๐Ÿš€

์•ˆ๋…•! ์˜ค๋Š˜์€ ์ •๋ง ์‹ค์šฉ์ ์ด๋ฉด์„œ๋„ ์—„์ฒญ ํ•ซํ•œ ์ฃผ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์™”์–ด. ๋ฐ”๋กœ AI๋ฅผ ํ™œ์šฉํ•ด์„œ ํˆฌ์ž ๋ฆฌํฌํŠธ๋ฅผ ์ž๋™์œผ๋กœ ๋ถ„์„ํ•˜๊ณ  ํ•ต์‹ฌ ๋‚ด์šฉ๋งŒ ์™์™ ๋ฝ‘์•„๋‚ด๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ๐Ÿ“Šโœจ

์š”์ฆ˜ ํˆฌ์žํ•˜๋Š” ์‚ฌ๋žŒ๋“ค ์ •๋ง ๋งŽ์ž–์•„? ์ฃผ์‹, ์ฝ”์ธ, ๋ถ€๋™์‚ฐ... ๊ทผ๋ฐ ์ฆ๊ถŒ์‚ฌ๋‚˜ ํˆฌ์ž ๊ธฐ๊ด€์—์„œ ๋‚˜์˜ค๋Š” ๋ฆฌํฌํŠธ๋“ค ๋ณด๋ฉด ๋จธ๋ฆฌ ์•„ํ”„์ง€ ์•Š์•„? ์ˆ˜์‹ญ ํŽ˜์ด์ง€์— ๋‹ฌํ•˜๋Š” PDF ํŒŒ์ผ, ๋ณต์žกํ•œ ์ฐจํŠธ, ์ „๋ฌธ ์šฉ์–ด ๊ฐ€๋“ํ•œ ๋ฌธ์žฅ๋“ค... ๐Ÿ˜ต

"์•„ ์ด๊ฑฐ ์–ธ์ œ ๋‹ค ์ฝ์–ด..." ํ•˜๋‹ค๊ฐ€ ๊ฒฐ๊ตญ ์ œ๋ชฉ๋งŒ ๋ณด๊ณ  ๋„˜์–ด๊ฐ€๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋Œ€๋ถ€๋ถ„์ด์ง€. ๊ทธ๋Ÿฐ๋ฐ ๋ง์ด์•ผ, AI ์ž๋™ํ™” ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•˜๋ฉด ์ด ๋ชจ๋“  ๊ฑธ ๋ช‡ ์ดˆ ๋งŒ์— ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

AI ํˆฌ์ž๋ฆฌํฌํŠธ ๋ถ„์„ ํ”„๋กœ์„ธ์Šค ํˆฌ์ž ๋ฆฌํฌํŠธ ๋ณต์žกํ•œ ๋‚ด์šฉ AI ๋ถ„์„ ํ•ต์‹ฌ ์š”์•ฝ AI ์ฒ˜๋ฆฌ ๋‹จ๊ณ„ 1 ํ…์ŠคํŠธ ์ถ”์ถœ 2 ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ 3 ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ ์ถ”์ถœ 4 ์š”์•ฝ๋ฌธ ์ƒ์„ฑ โšก ์ˆ˜์‹ญ ํŽ˜์ด์ง€ ๋ฆฌํฌํŠธ๋ฅผ ๋ช‡ ์ดˆ ๋งŒ์— ํ•ต์‹ฌ๋งŒ ์ถ”์ถœ! โšก

๐Ÿค” ์™œ AI ํˆฌ์ž๋ฆฌํฌํŠธ ๋ถ„์„์ด ํ•„์š”ํ• ๊นŒ?

๋จผ์ € ํ˜„์‹ค์ ์ธ ์ด์•ผ๊ธฐ๋ถ€ํ„ฐ ํ•ด๋ณผ๊ฒŒ. ์š”์ฆ˜ ํˆฌ์ž ์‹œ์žฅ์ด ์–ผ๋งˆ๋‚˜ ๋ณต์žกํ•ด์กŒ๋Š”์ง€ ์•Œ์•„? ๐Ÿ“ˆ

์ผ๋ฐ˜ ๊ฐœ์ธ ํˆฌ์ž์ž๊ฐ€ ๋งˆ์ฃผํ•˜๋Š” ํ˜„์‹ค:
โ€ข ํ•˜๋ฃจ์—๋„ ์ˆ˜์‹ญ ๊ฐœ์˜ ํˆฌ์ž ๋ฆฌํฌํŠธ๊ฐ€ ์Ÿ์•„์ง
โ€ข ํ•œ ๋ฆฌํฌํŠธ๋‹น ํ‰๊ท  20~50ํŽ˜์ด์ง€
โ€ข ์ „๋ฌธ ์šฉ์–ด์™€ ๋ณต์žกํ•œ ์žฌ๋ฌด ์ง€ํ‘œ๋กœ ๊ฐ€๋“
โ€ข ํ•ต์‹ฌ ๋‚ด์šฉ์„ ์ฐพ๊ธฐ ์œ„ํ•ด ์ „์ฒด๋ฅผ ๋‹ค ์ฝ์–ด์•ผ ํ•จ
โ€ข ์‹œ๊ฐ„์€ ์—†๊ณ , ๋†“์น˜๋ฉด ๊ธฐํšŒ๋„ ๋‚ ์•„๊ฐ โฐ

์‹ค์ œ๋กœ ์ฆ๊ถŒ์‚ฌ ์• ๋„๋ฆฌ์ŠคํŠธ๋“ค์ด ์ž‘์„ฑํ•œ ๋ฆฌํฌํŠธ๋ฅผ ๋ณด๋ฉด, ์ •๋ง ์ค‘์š”ํ•œ ๋‚ด์šฉ์€ ์ „์ฒด์˜ 10~20% ์ •๋„์•ผ. ๋‚˜๋จธ์ง€๋Š” ๋ฐฐ๊ฒฝ ์„ค๋ช…, ์‹œ์žฅ ์ƒํ™ฉ, ๋ฉด์ฑ… ์กฐํ•ญ ๊ฐ™์€ ๊ฑฐ์ง€. ๊ทผ๋ฐ ๊ทธ 10~20%๋ฅผ ์ฐพ์œผ๋ ค๋ฉด ์ „์ฒด๋ฅผ ๋‹ค ์ฝ์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒŒ ํ•จ์ •์ด์•ผ. ๐Ÿ˜…

๐Ÿ’ก ์‹ค์ œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” ๋ฌธ์ œ์  ์‚ผ์„ฑ์ „์ž ํˆฌ์ž ๋ฆฌํฌํŠธ ํ•˜๋‚˜๋ฅผ ์˜ˆ๋กœ ๋“ค์–ด๋ณผ๊ฒŒ:
โ€ข ์ด ํŽ˜์ด์ง€: 45ํŽ˜์ด์ง€
โ€ข ์ฝ๋Š” ๋ฐ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„: ์•ฝ 30~40๋ถ„
โ€ข ์‹ค์ œ ํˆฌ์ž ํŒ๋‹จ์— ํ•„์š”ํ•œ ํ•ต์‹ฌ ์ •๋ณด: 3~4ํŽ˜์ด์ง€ ๋ถ„๋Ÿ‰
โ€ข ํšจ์œจ์„ฑ: ์•ฝ 10% ๋ฏธ๋งŒ ๐Ÿ˜ฑ

๋งŒ์•ฝ ํ•˜๋ฃจ์— 5๊ฐœ์˜ ๋ฆฌํฌํŠธ๋ฅผ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค๋ฉด? 2~3์‹œ๊ฐ„์ด ํ›Œ์ฉ ๋‚ ์•„๊ฐ€๋Š” ๊ฑฐ์ง€. ์ง์žฅ์ธ์ด๋‚˜ ๋ฐ”์œ ์‚ฌ๋žŒ๋“ค์€ ์‚ฌ์‹ค์ƒ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ผ์ด์•ผ.

๐Ÿค– AI ์—์ด์ „ํŠธ๊ฐ€ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ AI๊ฐ€ ์–ด๋–ป๊ฒŒ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž! ๐Ÿš€

AI ์ž๋™ํ™” ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ:

1๏ธโƒฃ ๋ฌธ์„œ ์ž๋™ ์ˆ˜์ง‘ ๋ฐ ์ „์ฒ˜๋ฆฌ

๐Ÿ“ฅ์ž๋™ ํฌ๋กค๋ง ์ฆ๊ถŒ์‚ฌ ์›น์‚ฌ์ดํŠธ, ๊ธˆ์œต ํฌํ„ธ, ์ด๋ฉ”์ผ ๋“ฑ์—์„œ ์ƒˆ๋กœ์šด ๋ฆฌํฌํŠธ๋ฅผ ์ž๋™์œผ๋กœ ์ˆ˜์ง‘ํ•ด. ๋งค์ผ ์•„์นจ ์ถœ๊ทผํ•˜๋ฉด ์ด๋ฏธ ์ตœ์‹  ๋ฆฌํฌํŠธ๋“ค์ด ์ •๋ฆฌ๋˜์–ด ์žˆ๋Š” ๊ฑฐ์ง€!

๐Ÿ“„PDF ํŒŒ์‹ฑ
๋Œ€๋ถ€๋ถ„์˜ ํˆฌ์ž ๋ฆฌํฌํŠธ๋Š” PDF ํ˜•ํƒœ์•ผ. AI๋Š” ์ด PDF์—์„œ ํ…์ŠคํŠธ, ํ‘œ, ์ฐจํŠธ๋ฅผ ์ž๋™์œผ๋กœ ์ถ”์ถœํ•˜๊ณ  ๊ตฌ์กฐํ™”ํ•ด. ์‹ฌ์ง€์–ด ์Šค์บ”๋œ ์ด๋ฏธ์ง€ PDF๋„ OCR ๊ธฐ์ˆ ๋กœ ํ…์ŠคํŠธ๋ฅผ ๋ฝ‘์•„๋‚ผ ์ˆ˜ ์žˆ์–ด!

๐Ÿงน๋ฐ์ดํ„ฐ ์ •์ œ
๋ถˆํ•„์š”ํ•œ ํ—ค๋”, ํ‘ธํ„ฐ, ๊ด‘๊ณ , ๋ฉด์ฑ… ์กฐํ•ญ ๊ฐ™์€ ๊ฑด ์ž๋™์œผ๋กœ ์ œ๊ฑฐํ•˜๊ณ , ์‹ค์ œ ๋ถ„์„ ๋‚ด์šฉ๋งŒ ๋‚จ๊ฒจ๋‘ฌ.

2๏ธโƒฃ ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ(NLP)๋ฅผ ํ†ตํ•œ ๋‚ด์šฉ ๋ถ„์„

์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ๊ฐ€ ์ง„์งœ AI์˜ ๋งˆ๋ฒ•์ด ์‹œ์ž‘๋˜๋Š” ๋ถ€๋ถ„์ด์•ผ! โœจ

ํ•ต์‹ฌ ๊ธฐ์ˆ ๋“ค:

๐ŸŽฏ ๊ฐœ์ฒด๋ช… ์ธ์‹(NER)

๋ฌธ์„œ์—์„œ ์ค‘์š”ํ•œ ์ •๋ณด๋ฅผ ์ž๋™์œผ๋กœ ์ฐพ์•„๋‚ด:
โ€ข ๊ธฐ์—…๋ช…: "์‚ผ์„ฑ์ „์ž", "SKํ•˜์ด๋‹‰์Šค"
โ€ข ์žฌ๋ฌด ์ง€ํ‘œ: "์˜์—…์ด์ต", "๋งค์ถœ์•ก", "PER"
โ€ข ๋‚ ์งœ: "2024๋…„ 1๋ถ„๊ธฐ", "2025๋…„ ์ „๋ง"
โ€ข ๊ธˆ์•ก: "10์กฐ์›", "15% ์ฆ๊ฐ€"

์ด๋ ‡๊ฒŒ ์ถ”์ถœ๋œ ์ •๋ณด๋“ค์€ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋ผ์„œ ๋‚˜์ค‘์— ๊ฒ€์ƒ‰ํ•˜๊ฑฐ๋‚˜ ๋น„๊ต ๋ถ„์„ํ•  ๋•Œ ์—„์ฒญ ์œ ์šฉํ•ด! ๐Ÿ“Š

๐Ÿ’ฌ ๊ฐ์„ฑ ๋ถ„์„(Sentiment Analysis)

์• ๋„๋ฆฌ์ŠคํŠธ๊ฐ€ ํ•ด๋‹น ์ข…๋ชฉ์— ๋Œ€ํ•ด ๊ธ์ •์ ์ธ์ง€, ๋ถ€์ •์ ์ธ์ง€, ์ค‘๋ฆฝ์ ์ธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ํŒ๋‹จํ•ด:

๊ธ์ •์  ํ‘œํ˜„:
"์‹ค์  ๊ฐœ์„  ๊ธฐ๋Œ€", "๋ชฉํ‘œ์ฃผ๊ฐ€ ์ƒํ–ฅ", "๋งค์ˆ˜ ์ถ”์ฒœ" โ†’ ๐Ÿ˜Š ๊ธ์ • ์ ์ˆ˜ +0.8

๋ถ€์ •์  ํ‘œํ˜„:
"์‹ค์  ๋ถ€์ง„ ์šฐ๋ ค", "๋ชฉํ‘œ์ฃผ๊ฐ€ ํ•˜ํ–ฅ", "๋น„์ค‘ ์ถ•์†Œ" โ†’ ๐Ÿ˜Ÿ ๋ถ€์ • ์ ์ˆ˜ -0.6

์ด๋Ÿฐ ๊ฐ์„ฑ ์ ์ˆ˜๋ฅผ ํ†ตํ•ด ๋ฆฌํฌํŠธ์˜ ์ „๋ฐ˜์ ์ธ ํ†ค์„ ํ•œ๋ˆˆ์— ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿ” ํ•ต์‹ฌ ๋ฌธ์žฅ ์ถ”์ถœ

์ „์ฒด ๋ฌธ์„œ์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ฌธ์žฅ๋“ค์„ ์ž๋™์œผ๋กœ ๊ณจ๋ผ๋‚ด๋Š” ๊ธฐ์ˆ ์ด์•ผ. ์–ด๋–ป๊ฒŒ ํŒ๋‹จํ•˜๋ƒ๊ณ ?

์ค‘์š”๋„ ํŒ๋‹จ ๊ธฐ์ค€:
โ€ข ๋ฌธ์žฅ์˜ ์œ„์น˜ (์„œ๋ก , ๊ฒฐ๋ก  ๋ถ€๋ถ„์˜ ๋ฌธ์žฅ์€ ์ค‘์š”๋„ ๋†’์Œ)
โ€ข ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ ํฌํ•จ ์—ฌ๋ถ€
โ€ข ์ˆ˜์น˜ ์ •๋ณด ํฌํ•จ ์—ฌ๋ถ€
โ€ข ๋ฌธ์žฅ ๊ฐ„ ์—ฐ๊ฒฐ์„ฑ
โ€ข ์ „๋ฌธ ์šฉ์–ด ๋ฐ€๋„

์˜ˆ๋ฅผ ๋“ค์–ด, "2024๋…„ 1๋ถ„๊ธฐ ์˜์—…์ด์ต์€ ์ „๋…„ ๋Œ€๋น„ 25% ์ฆ๊ฐ€ํ•œ 3์กฐ์›์„ ๊ธฐ๋กํ•  ๊ฒƒ์œผ๋กœ ์ „๋ง๋œ๋‹ค"๋ผ๋Š” ๋ฌธ์žฅ์€ ๋†’์€ ์ค‘์š”๋„ ์ ์ˆ˜๋ฅผ ๋ฐ›๊ฒŒ ๋ผ! ๐ŸŽฏ

3๏ธโƒฃ ๋”ฅ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ์š”์•ฝ ์ƒ์„ฑ

์ด์ œ ๊ฐ€์žฅ ํ•ต์‹ฌ์ ์ธ ๋ถ€๋ถ„, ๋ฐ”๋กœ ์ž๋™ ์š”์•ฝ์ด์•ผ! ๐ŸŽจ

์ตœ๊ทผ์—๋Š” GPT, BERT, T5 ๊ฐ™์€ ๋Œ€๊ทœ๋ชจ ์–ธ์–ด ๋ชจ๋ธ๋“ค์ด ์—„์ฒญ๋‚œ ์„ฑ๋Šฅ์„ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ์–ด. ์ด๋Ÿฐ ๋ชจ๋ธ๋“ค์„ ํ™œ์šฉํ•˜๋ฉด:

โœ… ์ถ”์ถœ ์š”์•ฝ(Extractive Summarization) ์›๋ฌธ์—์„œ ์ค‘์š”ํ•œ ๋ฌธ์žฅ๋“ค์„ ๊ทธ๋Œ€๋กœ ๋ฝ‘์•„์„œ ์กฐํ•ฉํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ. ์›๋ฌธ์˜ ์ •ํ™•์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์ง€!

์˜ˆ์‹œ:
์›๋ฌธ 45ํŽ˜์ด์ง€ โ†’ ํ•ต์‹ฌ ๋ฌธ์žฅ 15๊ฐœ ์ถ”์ถœ โ†’ 2ํŽ˜์ด์ง€ ์š”์•ฝ๋ณธ ์ƒ์„ฑ

โœ… ์ƒ์„ฑ ์š”์•ฝ(Abstractive Summarization)
AI๊ฐ€ ๋‚ด์šฉ์„ ์ดํ•ดํ•˜๊ณ  ์ƒˆ๋กœ์šด ๋ฌธ์žฅ์œผ๋กœ ์žฌ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ. ๋” ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์ฝ๊ธฐ ํŽธํ•œ ์š”์•ฝ๋ฌธ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด!

์˜ˆ์‹œ:
"์‚ผ์„ฑ์ „์ž์˜ 2024๋…„ 1๋ถ„๊ธฐ ์‹ค์ ์€ ๋ฉ”๋ชจ๋ฆฌ ๋ฐ˜๋„์ฒด ๊ฐ€๊ฒฉ ์ƒ์Šน๊ณผ ์Šค๋งˆํŠธํฐ ํŒ๋งค ์ฆ๊ฐ€๋กœ ์ „๋…„ ๋Œ€๋น„ ํฌ๊ฒŒ ๊ฐœ์„ ๋  ์ „๋ง์ด๋ฉฐ, ๋ชฉํ‘œ์ฃผ๊ฐ€๋Š” 85,000์›์œผ๋กœ ์ƒํ–ฅ ์กฐ์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค." ๐Ÿ“ฑ๐Ÿ’พ

๐Ÿ› ๏ธ ์‹ค์ œ ๊ตฌํ˜„ ๋ฐฉ๋ฒ• - ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ

์ž, ์ด๋ก ์€ ์ด ์ •๋„๋ฉด ์ถฉ๋ถ„ํ•˜๊ณ , ์ด์ œ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๋งŒ๋“œ๋Š”์ง€ ์•Œ์•„๋ณด์ž! ์ฝ”๋”ฉ์ด ์ฒ˜์Œ์ด๋ผ๋„ ๊ฑฑ์ • ๋งˆ. ์ฐจ๊ทผ์ฐจ๊ทผ ์„ค๋ช…ํ•ด์ค„๊ฒŒ! ๐Ÿ’ช

๐Ÿ”ง ํ•„์š”ํ•œ ๋„๊ตฌ์™€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๊ธฐ๋ณธ ํ™˜๊ฒฝ: โ€ข Python 3.8 ์ด์ƒ
โ€ข ๊ฐ€์ƒํ™˜๊ฒฝ (venv ๋˜๋Š” conda)
โ€ข ํ…์ŠคํŠธ ์—๋””ํ„ฐ (VS Code ์ถ”์ฒœ)

ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ:
โ€ข PyPDF2 ๋˜๋Š” pdfplumber: PDF ํŒŒ์‹ฑ
โ€ข beautifulsoup4: ์›น ํฌ๋กค๋ง
โ€ข transformers: ํ—ˆ๊น…ํŽ˜์ด์Šค ๋ชจ๋ธ ํ™œ์šฉ
โ€ข openai: GPT API ์‚ฌ์šฉ
โ€ข pandas: ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
โ€ข nltk ๋˜๋Š” spacy: ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ
โ€ข selenium: ๋™์  ์›นํŽ˜์ด์ง€ ํฌ๋กค๋ง

๐Ÿ“ Step 1: PDF์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœํ•˜๊ธฐ

๊ฐ€์žฅ ๋จผ์ € ํ•ด์•ผ ํ•  ์ผ์€ PDF ํŒŒ์ผ์—์„œ ํ…์ŠคํŠธ๋ฅผ ๋ฝ‘์•„๋‚ด๋Š” ๊ฑฐ์•ผ. ์ƒ๊ฐ๋ณด๋‹ค ๊ฐ„๋‹จํ•ด!

import pdfplumber
import re

def extract_text_from_pdf(pdf_path):
    """PDF ํŒŒ์ผ์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•˜๋Š” ํ•จ์ˆ˜"""
    text = ""
    
    with pdfplumber.open(pdf_path) as pdf:
        for page in pdf.pages:
            # ๊ฐ ํŽ˜์ด์ง€์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ
            page_text = page.extract_text()
            if page_text:
                text += page_text + "\n"
    
    # ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ ์ œ๊ฑฐ
    text = re.sub(r'\n+', '\n', text)
    text = re.sub(r' +', ' ', text)
    
    return text

# ์‚ฌ์šฉ ์˜ˆ์‹œ
report_text = extract_text_from_pdf("์‚ผ์„ฑ์ „์ž_ํˆฌ์ž๋ฆฌํฌํŠธ.pdf")
print(f"์ถ”์ถœ๋œ ํ…์ŠคํŠธ ๊ธธ์ด: {len(report_text)} ๊ธ€์ž")

๐Ÿ’ก Pro Tip: PDF์— ํ‘œ๋‚˜ ์ฐจํŠธ๊ฐ€ ๋งŽ๋‹ค๋ฉด pdfplumber์˜ extract_tables() ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ‘œ ๋ฐ์ดํ„ฐ๋„ ๋”ฐ๋กœ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ์–ด! ์žฌ๋ฌด์ œํ‘œ ๊ฐ™์€ ๊ฑด ํ‘œ ํ˜•ํƒœ๋กœ ์ถ”์ถœํ•˜๋Š” ๊ฒŒ ํ›จ์”ฌ ์ •ํ™•ํ•ด. ๐Ÿ“Š

๐Ÿ“ Step 2: ํ…์ŠคํŠธ ์ „์ฒ˜๋ฆฌ ๋ฐ ๊ตฌ์กฐํ™”

์ถ”์ถœํ•œ ํ…์ŠคํŠธ๋ฅผ AI๊ฐ€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์ •๋ฆฌํ•ด์•ผ ํ•ด. ์ด ๊ณผ์ •์ด ์ƒ๊ฐ๋ณด๋‹ค ์ค‘์š”ํ•ด!

import re
from typing import Dict, List

def preprocess_report(text: str) -> Dict[str, any]:
    """๋ฆฌํฌํŠธ ํ…์ŠคํŠธ๋ฅผ ๊ตฌ์กฐํ™”ํ•˜๋Š” ํ•จ์ˆ˜"""
    
    # ์„น์…˜ ๊ตฌ๋ถ„ (์ผ๋ฐ˜์ ์ธ ๋ฆฌํฌํŠธ ๊ตฌ์กฐ)
    sections = {
        'title': '',
        'summary': '',
        'company_info': '',
        'financial_analysis': '',
        'investment_opinion': '',
        'target_price': '',
        'risks': ''
    }
    
    # ์ œ๋ชฉ ์ถ”์ถœ (๋ณดํ†ต ์ฒซ ์ค„)
    lines = text.split('\n')
    sections['title'] = lines[0] if lines else ''
    
    # ํˆฌ์ž์˜๊ฒฌ ์ถ”์ถœ (๋งค์ˆ˜, ๋งค๋„, ์ค‘๋ฆฝ ๋“ฑ)
    opinion_pattern = r'(ํˆฌ์ž์˜๊ฒฌ|ํˆฌ์ž๋“ฑ๊ธ‰|Investment Rating)[:\s]*(๋งค์ˆ˜|๋งค๋„|์ค‘๋ฆฝ|BUY|SELL|HOLD)'
    opinion_match = re.search(opinion_pattern, text, re.IGNORECASE)
    if opinion_match:
        sections['investment_opinion'] = opinion_match.group(2)
    
    # ๋ชฉํ‘œ์ฃผ๊ฐ€ ์ถ”์ถœ
    target_pattern = r'๋ชฉํ‘œ์ฃผ๊ฐ€[:\s]*([0-9,]+)\s*์›'
    target_match = re.search(target_pattern, text)
    if target_match:
        sections['target_price'] = target_match.group(1)
    
    # ํ•ต์‹ฌ ์žฌ๋ฌด ์ง€ํ‘œ ์ถ”์ถœ
    financial_keywords = ['์˜์—…์ด์ต', '๋งค์ถœ์•ก', 'EPS', 'PER', 'PBR', 'ROE']
    financial_sentences = []
    
    for line in lines:
        if any(keyword in line for keyword in financial_keywords):
            financial_sentences.append(line.strip())
    
    sections['financial_analysis'] = '\n'.join(financial_sentences)
    
    return sections

# ์‚ฌ์šฉ ์˜ˆ์‹œ
structured_data = preprocess_report(report_text)
print("ํˆฌ์ž์˜๊ฒฌ:", structured_data['investment_opinion'])
print("๋ชฉํ‘œ์ฃผ๊ฐ€:", structured_data['target_price'])

๐Ÿ“ Step 3: AI ๋ชจ๋ธ์„ ํ™œ์šฉํ•œ ์š”์•ฝ ์ƒ์„ฑ

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ AI๋ฅผ ํ™œ์šฉํ•  ์ฐจ๋ก€์•ผ! ์—ฌ๊ธฐ์„œ๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ• ๊ฒŒ. ๐Ÿค–

๋ฐฉ๋ฒ• 1: OpenAI GPT API ํ™œ์šฉ (์ถ”์ฒœ! ๐ŸŒŸ)

import openai
from typing import Dict

# API ํ‚ค ์„ค์ • (์‹ค์ œ ์‚ฌ์šฉ ์‹œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜์„ธ์š”!)
openai.api_key = "your-api-key-here"

def summarize_with_gpt(report_text: str, max_length: int = 500) -> Dict[str, str]:
    """GPT๋ฅผ ์‚ฌ์šฉํ•ด ํˆฌ์ž ๋ฆฌํฌํŠธ๋ฅผ ์š”์•ฝํ•˜๋Š” ํ•จ์ˆ˜"""
    
    # ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง์ด ํ•ต์‹ฌ!
    prompt = f"""
    ๋‹ค์Œ์€ ํˆฌ์ž ๋ฆฌํฌํŠธ์˜ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. ์ด ๋ฆฌํฌํŠธ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ๋‹ค์Œ ํ•ญ๋ชฉ๋“ค์„ ์ถ”์ถœํ•ด์ฃผ์„ธ์š”:
    
    1. ํ•ต์‹ฌ ํˆฌ์ž ํฌ์ธํŠธ (3๊ฐ€์ง€)
    2. ํˆฌ์ž์˜๊ฒฌ ๋ฐ ๋ชฉํ‘œ์ฃผ๊ฐ€
    3. ์ฃผ์š” ์žฌ๋ฌด ์ „๋ง
    4. ๋ฆฌ์Šคํฌ ์š”์ธ
    
    ๋ฆฌํฌํŠธ ๋‚ด์šฉ:
    {report_text[:4000]}  # GPT ํ† ํฐ ์ œํ•œ ๊ณ ๋ ค
    
    ์œ„ ๋‚ด์šฉ์„ {max_length}์ž ์ด๋‚ด๋กœ ๋ช…ํ™•ํ•˜๊ณ  ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์š”์•ฝํ•ด์ฃผ์„ธ์š”.
    """
    
    try:
        response = openai.ChatCompletion.create(
            model="gpt-4",  # ๋˜๋Š” "gpt-3.5-turbo"
            messages=[
                {"role": "system", "content": "๋‹น์‹ ์€ ์ „๋ฌธ ๊ธˆ์œต ์• ๋„๋ฆฌ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. ํˆฌ์ž ๋ฆฌํฌํŠธ๋ฅผ ์ •ํ™•ํ•˜๊ณ  ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์š”์•ฝํ•˜๋Š” ๊ฒƒ์ด ํŠน๊ธฐ์ž…๋‹ˆ๋‹ค."},
                {"role": "user", "content": prompt}
            ],
            temperature=0.3,  # ๋‚ฎ์„์ˆ˜๋ก ์ผ๊ด€์„ฑ ์žˆ๋Š” ๋‹ต๋ณ€
            max_tokens=1000
        )
        
        summary = response.choices[0].message.content
        
        # ๊ฒฐ๊ณผ๋ฅผ ๊ตฌ์กฐํ™”
        result = {
            'summary': summary,
            'model': 'gpt-4',
            'tokens_used': response.usage.total_tokens
        }
        
        return result
        
    except Exception as e:
        print(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
        return {'summary': '', 'error': str(e)}

# ์‚ฌ์šฉ ์˜ˆ์‹œ
summary_result = summarize_with_gpt(report_text)
print("=== AI ์š”์•ฝ ๊ฒฐ๊ณผ ===")
print(summary_result['summary'])
print(f"\n์‚ฌ์šฉ๋œ ํ† ํฐ: {summary_result['tokens_used']}")

โš ๏ธ ๋น„์šฉ ๊ด€๋ จ ์ฃผ์˜์‚ฌํ•ญ OpenAI API๋Š” ์œ ๋ฃŒ์•ผ. GPT-4๋Š” ํ† ํฐ๋‹น ๋น„์šฉ์ด ์ข€ ์žˆ์–ด์„œ, ๋งŽ์€ ๋ฆฌํฌํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋น„์šฉ์ด ์Œ“์ผ ์ˆ˜ ์žˆ์–ด.

๋น„์šฉ ์ ˆ๊ฐ ํŒ:
โ€ข GPT-3.5-turbo ์‚ฌ์šฉ (GPT-4๋ณด๋‹ค 10๋ฐฐ ์ €๋ ด)
โ€ข ์ž…๋ ฅ ํ…์ŠคํŠธ๋ฅผ ๋ฏธ๋ฆฌ ์••์ถ• (ํ•ต์‹ฌ ๋ถ€๋ถ„๋งŒ ์ „๋‹ฌ)
โ€ข ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋กœ ํšจ์œจ์„ฑ ๋†’์ด๊ธฐ
โ€ข ์บ์‹ฑ ํ™œ์šฉ (๊ฐ™์€ ๋ฆฌํฌํŠธ ์žฌ์š”์•ฝ ๋ฐฉ์ง€)

ํ•œ ๋‹ฌ์— 100๊ฐœ ๋ฆฌํฌํŠธ ์ •๋„ ์ฒ˜๋ฆฌํ•œ๋‹ค๋ฉด ๋Œ€๋žต $10~30 ์ •๋„ ์˜ˆ์ƒํ•˜๋ฉด ๋ผ!

๋ฐฉ๋ฒ• 2: ์˜คํ”ˆ์†Œ์Šค ๋ชจ๋ธ ํ™œ์šฉ (๋ฌด๋ฃŒ! ๐Ÿ’ฐ)

๋น„์šฉ์ด ๋ถ€๋‹ด๋œ๋‹ค๋ฉด ํ—ˆ๊น…ํŽ˜์ด์Šค์˜ ์˜คํ”ˆ์†Œ์Šค ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์–ด. ์„ฑ๋Šฅ์€ GPT๋ณด๋‹ค ์กฐ๊ธˆ ๋–จ์–ด์ง€์ง€๋งŒ, ๋ฌด๋ฃŒ๊ณ  ์ž์ฒด ์„œ๋ฒ„์—์„œ ๋Œ๋ฆด ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์ง€!

from transformers import pipeline
import torch

def summarize_with_opensource(text: str, max_length: int = 500) -> str:
    """์˜คํ”ˆ์†Œ์Šค ๋ชจ๋ธ๋กœ ์š”์•ฝํ•˜๋Š” ํ•จ์ˆ˜"""
    
    # GPU ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ
    device = 0 if torch.cuda.is_available() else -1
    
    # ํ•œ๊ตญ์–ด ์š”์•ฝ ๋ชจ๋ธ ๋กœ๋“œ
    # ์˜ˆ: "eenzeenee/t5-base-korean-summarization"
    summarizer = pipeline(
        "summarization",
        model="facebook/bart-large-cnn",  # ์˜์–ด ๋ชจ๋ธ
        device=device
    )
    
    # ํ…์ŠคํŠธ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ๋ฉด ์ฒญํฌ๋กœ ๋‚˜๋ˆ„๊ธฐ
    max_chunk_length = 1024
    chunks = [text[i:i+max_chunk_length] 
              for i in range(0, len(text), max_chunk_length)]
    
    summaries = []
    for chunk in chunks[:3]:  # ์ฒ˜์Œ 3๊ฐœ ์ฒญํฌ๋งŒ ์ฒ˜๋ฆฌ
        try:
            summary = summarizer(
                chunk,
                max_length=max_length,
                min_length=50,
                do_sample=False
            )
            summaries.append(summary[0]['summary_text'])
        except Exception as e:
            print(f"์ฒญํฌ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}")
            continue
    
    # ๋ชจ๋“  ์š”์•ฝ์„ ํ•ฉ์น˜๊ธฐ
    final_summary = " ".join(summaries)
    
    return final_summary

# ์‚ฌ์šฉ ์˜ˆ์‹œ
opensource_summary = summarize_with_opensource(report_text)
print("=== ์˜คํ”ˆ์†Œ์Šค ๋ชจ๋ธ ์š”์•ฝ ===")
print(opensource_summary)

๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด ๋ชจ๋ธ ์ถ”์ฒœ: ํ•œ๊ตญ์–ด ํˆฌ์ž ๋ฆฌํฌํŠธ๋ฅผ ๋‹ค๋ฃฌ๋‹ค๋ฉด ํ•œ๊ตญ์–ด์— ํŠนํ™”๋œ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„:
โ€ข eenzeenee/t5-base-korean-summarization
โ€ข psyche/KoT5-summarization
โ€ข lcw99/t5-base-korean-text-summary

์ด๋Ÿฐ ๋ชจ๋ธ๋“ค์€ ํ•œ๊ตญ์–ด ๋ฌธ๋ฒ•๊ณผ ๊ธˆ์œต ์šฉ์–ด๋ฅผ ๋” ์ž˜ ์ดํ•ดํ•ด!

๐Ÿ“ Step 4: ํ•ต์‹ฌ ์ •๋ณด ์ถ”์ถœ ๋ฐ ๊ตฌ์กฐํ™”

์š”์•ฝ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•ด. ํˆฌ์ž ํŒ๋‹จ์— ํ•„์š”ํ•œ ๊ตฌ์ฒด์ ์ธ ์ •๋ณด๋“ค์„ ๋”ฐ๋กœ ๋ฝ‘์•„๋‚ด์•ผ ํ•ด! ๐ŸŽฏ

import re
from datetime import datetime
from typing import Dict, List

class InvestmentReportAnalyzer:
    """ํˆฌ์ž ๋ฆฌํฌํŠธ ๋ถ„์„ ํด๋ž˜์Šค"""
    
    def __init__(self, text: str):
        self.text = text
        self.analysis = {}
    
    def extract_key_metrics(self) -> Dict[str, any]:
        """ํ•ต์‹ฌ ์ง€ํ‘œ ์ถ”์ถœ"""
        metrics = {}
        
        # ๋ชฉํ‘œ์ฃผ๊ฐ€ ์ถ”์ถœ
        target_price_pattern = r'๋ชฉํ‘œ์ฃผ?๊ฐ€[:\s]*([0-9,]+)\s*์›'
        match = re.search(target_price_pattern, self.text)
        if match:
            metrics['target_price'] = int(match.group(1).replace(',', ''))
        
        # ํ˜„์žฌ์ฃผ๊ฐ€ ์ถ”์ถœ
        current_price_pattern = r'ํ˜„์žฌ์ฃผ?๊ฐ€[:\s]*([0-9,]+)\s*์›'
        match = re.search(current_price_pattern, self.text)
        if match:
            metrics['current_price'] = int(match.group(1).replace(',', ''))
        
        # ์ƒ์Šน์—ฌ๋ ฅ ๊ณ„์‚ฐ
        if 'target_price' in metrics and 'current_price' in metrics:
            upside = ((metrics['target_price'] - metrics['current_price']) 
                     / metrics['current_price'] * 100)
            metrics['upside_potential'] = round(upside, 2)
        
        # EPS ์ถ”์ถœ
        eps_pattern = r'EPS[:\s]*([0-9,]+)\s*์›'
        match = re.search(eps_pattern, self.text)
        if match:
            metrics['eps'] = int(match.group(1).replace(',', ''))
        
        # PER ์ถ”์ถœ
        per_pattern = r'PER[:\s]*([0-9.]+)\s*๋ฐฐ'
        match = re.search(per_pattern, self.text)
        if match:
            metrics['per'] = float(match.group(1))
        
        return metrics
    
    def extract_investment_opinion(self) -> Dict[str, str]:
        """ํˆฌ์ž์˜๊ฒฌ ์ถ”์ถœ"""
        opinion_data = {}
        
        # ํˆฌ์ž์˜๊ฒฌ ๋งคํ•‘
        opinion_map = {
            '๋งค์ˆ˜': 'BUY',
            'BUY': 'BUY',
            '์ ๊ทน๋งค์ˆ˜': 'STRONG_BUY',
            '์ค‘๋ฆฝ': 'HOLD',
            'HOLD': 'HOLD',
            '๋งค๋„': 'SELL',
            'SELL': 'SELL'
        }
        
        # ํˆฌ์ž์˜๊ฒฌ ์ฐพ๊ธฐ
        for korean, english in opinion_map.items():
            if korean in self.text:
                opinion_data['opinion'] = english
                opinion_data['opinion_kr'] = korean
                break
        
        # ์ด์ „ ์˜๊ฒฌ๊ณผ ๋น„๊ต
        change_pattern = r'(์ƒํ–ฅ|ํ•˜ํ–ฅ|์œ ์ง€)'
        match = re.search(change_pattern, self.text)
        if match:
            opinion_data['change'] = match.group(1)
        
        return opinion_data
    
    def extract_financial_forecast(self) -> List[Dict]:
        """์žฌ๋ฌด ์ „๋ง ์ถ”์ถœ"""
        forecasts = []
        
        # ์—ฐ๋„๋ณ„ ์‹ค์  ์ „๋ง ํŒจํ„ด
        year_pattern = r'(20\d{2})๋…„.*?๋งค์ถœ.*?([0-9,]+).*?์˜์—…์ด์ต.*?([0-9,]+)'
        matches = re.finditer(year_pattern, self.text)
        
        for match in matches:
            forecast = {
                'year': match.group(1),
                'revenue': match.group(2).replace(',', ''),
                'operating_profit': match.group(3).replace(',', '')
            }
            forecasts.append(forecast)
        
        return forecasts
    
    def extract_key_points(self) -> List[str]:
        """ํ•ต์‹ฌ ํฌ์ธํŠธ ์ถ”์ถœ"""
        key_points = []
        
        # "ํ•ต์‹ฌ", "ํฌ์ธํŠธ", "์ฃผ๋ชฉ" ๊ฐ™์€ ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ๋Š” ๋ฌธ์žฅ ์ฐพ๊ธฐ
        keywords = ['ํ•ต์‹ฌ', 'ํฌ์ธํŠธ', '์ฃผ๋ชฉ', '์ฃผ์š”', 'ํŠน์ง•']
        sentences = self.text.split('.')
        
        for sentence in sentences:
            if any(keyword in sentence for keyword in keywords):
                cleaned = sentence.strip()
                if len(cleaned) > 20:  # ๋„ˆ๋ฌด ์งง์€ ๋ฌธ์žฅ ์ œ์™ธ
                    key_points.append(cleaned)
        
        return key_points[:5]  # ์ƒ์œ„ 5๊ฐœ๋งŒ
    
    def analyze(self) -> Dict[str, any]:
        """์ „์ฒด ๋ถ„์„ ์‹คํ–‰"""
        self.analysis = {
            'metrics': self.extract_key_metrics(),
            'opinion': self.extract_investment_opinion(),
            'forecast': self.extract_financial_forecast(),
            'key_points': self.extract_key_points(),
            'analyzed_at': datetime.now().isoformat()
        }
        
        return self.analysis

# ์‚ฌ์šฉ ์˜ˆ์‹œ
analyzer = InvestmentReportAnalyzer(report_text)
analysis_result = analyzer.analyze()

print("=== ๋ถ„์„ ๊ฒฐ๊ณผ ===")
print(f"๋ชฉํ‘œ์ฃผ๊ฐ€: {analysis_result['metrics'].get('target_price', 'N/A'):,}์›")
print(f"์ƒ์Šน์—ฌ๋ ฅ: {analysis_result['metrics'].get('upside_potential', 'N/A')}%")
print(f"ํˆฌ์ž์˜๊ฒฌ: {analysis_result['opinion'].get('opinion_kr', 'N/A')}")
print("\nํ•ต์‹ฌ ํฌ์ธํŠธ:")
for i, point in enumerate(analysis_result['key_points'], 1):
    print(f"{i}. {point}")

๐Ÿ”„ ์ž๋™ํ™” ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ

์ด์ œ ์ด ๋ชจ๋“  ๊ฑธ ์ž๋™์œผ๋กœ ๋Œ์•„๊ฐ€๊ฒŒ ๋งŒ๋“ค์–ด๋ณด์ž! ๋งค์ผ ์•„์นจ ์ž๋™์œผ๋กœ ์ƒˆ ๋ฆฌํฌํŠธ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ๋ถ„์„ํ•ด์„œ ์ด๋ฉ”์ผ์ด๋‚˜ ์Šฌ๋ž™์œผ๋กœ ๋ณด๋‚ด์ฃผ๋Š” ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ๊ฑฐ์•ผ. ๐Ÿค–โœ‰๏ธ

๐Ÿ• ์Šค์ผ€์ค„๋ง ์„ค์ •

import schedule
import time
from datetime import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

class AutomatedReportAnalyzer:
    """์ž๋™ํ™”๋œ ๋ฆฌํฌํŠธ ๋ถ„์„ ์‹œ์Šคํ…œ"""
    
    def __init__(self, email_config: Dict[str, str]):
        self.email_config = email_config
        self.reports_processed = 0
    
    def collect_new_reports(self) -> List[str]:
        """์ƒˆ๋กœ์šด ๋ฆฌํฌํŠธ ์ˆ˜์ง‘"""
        # ์‹ค์ œ๋กœ๋Š” ์›น ํฌ๋กค๋ง์ด๋‚˜ API ํ˜ธ์ถœ๋กœ ๊ตฌํ˜„
        # ์—ฌ๊ธฐ์„œ๋Š” ์˜ˆ์‹œ๋กœ ํŒŒ์ผ ์‹œ์Šคํ…œ ์ฒดํฌ
        import os
        
        reports_dir = "./reports"
        new_reports = []
        
        if os.path.exists(reports_dir):
            for filename in os.listdir(reports_dir):
                if filename.endswith('.pdf'):
                    filepath = os.path.join(reports_dir, filename)
                    # ์˜ค๋Š˜ ์ƒ์„ฑ๋œ ํŒŒ์ผ๋งŒ
                    if self._is_today_file(filepath):
                        new_reports.append(filepath)
        
        return new_reports
    
    def _is_today_file(self, filepath: str) -> bool:
        """ํŒŒ์ผ์ด ์˜ค๋Š˜ ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ"""
        import os
        from datetime import date
        
        file_time = os.path.getmtime(filepath)
        file_date = datetime.fromtimestamp(file_time).date()
        return file_date == date.today()
    
    def process_report(self, pdf_path: str) -> Dict[str, any]:
        """๋ฆฌํฌํŠธ ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ"""
        print(f"์ฒ˜๋ฆฌ ์ค‘: {pdf_path}")
        
        # 1. PDF์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ
        text = extract_text_from_pdf(pdf_path)
        
        # 2. ์ „์ฒ˜๋ฆฌ
        structured = preprocess_report(text)
        
        # 3. AI ์š”์•ฝ
        summary = summarize_with_gpt(text, max_length=300)
        
        # 4. ํ•ต์‹ฌ ์ •๋ณด ์ถ”์ถœ
        analyzer = InvestmentReportAnalyzer(text)
        analysis = analyzer.analyze()
        
        # ๊ฒฐ๊ณผ ํ†ตํ•ฉ
        result = {
            'filename': pdf_path.split('/')[-1],
            'summary': summary['summary'],
            'analysis': analysis,
            'processed_at': datetime.now().isoformat()
        }
        
        return result
    
    def send_email_report(self, results: List[Dict]):
        """์ด๋ฉ”์ผ๋กœ ๊ฒฐ๊ณผ ์ „์†ก"""
        if not results:
            print("์ „์†กํ•  ๋ฆฌํฌํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
            return
        
        # ์ด๋ฉ”์ผ ๋‚ด์šฉ ์ƒ์„ฑ
        html_content = self._generate_email_html(results)
        
        # ์ด๋ฉ”์ผ ์„ค์ •
        msg = MIMEMultipart('alternative')
        msg['Subject'] = f"๐Ÿ“Š ํˆฌ์ž ๋ฆฌํฌํŠธ ์š”์•ฝ - {datetime.now().strftime('%Y-%m-%d')}"
        msg['From'] = self.email_config['from']
        msg['To'] = self.email_config['to']
        
        html_part = MIMEText(html_content, 'html')
        msg.attach(html_part)
        
        # ์ด๋ฉ”์ผ ์ „์†ก
        try:
            with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
                server.login(
                    self.email_config['username'],
                    self.email_config['password']
                )
                server.send_message(msg)
            print("โœ… ์ด๋ฉ”์ผ ์ „์†ก ์™„๋ฃŒ!")
        except Exception as e:
            print(f"โŒ ์ด๋ฉ”์ผ ์ „์†ก ์‹คํŒจ: {e}")
    
    def _generate_email_html(self, results: List[Dict]) -> str:
        """์ด๋ฉ”์ผ HTML ์ƒ์„ฑ"""
        html = f"""
        <html>
        <head>
            <style>.tx-header{background:linear-gradient(135deg,#f5f7fa 0%,#e8ecf1 100%);
border-radius:16px;
padding:40px 30px;
text-align:center;
margin-bottom:40px;
box-shadow:0 4px 15px rgba(0,0,0,0.08)}
.tx-section{margin:50px 0;
line-height:1.9}
.tx-highlight{background:linear-gradient(120deg,#fef5e7 0%,#fdebd0 100%);
padding:25px;
border-radius:12px;
margin:30px 0;
border-left:5px solid #f39c12}
.tx-point-box{background:#f8f9fa;
padding:20px;
border-radius:10px;
margin:25px 0;
border-left:4px solid #3498db}
.tx-tip{background:linear-gradient(135deg,#e8f5e9 0%,#c8e6c9 100%);
padding:20px;
border-radius:10px;
margin:25px 0}
.tx-warning{background:linear-gradient(135deg,#fff3e0 0%,#ffe0b2 100%);
padding:20px;
border-radius:10px;
margin:25px 0}
.tx-table-wrap{overflow-x:auto;
margin:25px 0}
.tx-table{width:100%;
border-collapse:collapse;
background:white;
border-radius:8px;
overflow:hidden}
.tx-table th{background:#34495e;
color:white;
padding:15px;
text-align:left}
.tx-table td{padding:12px 15px;
border-bottom:1px solid #ecf0f1}
.tx-step{background:white;
padding:20px;
margin:20px 0;
border-radius:10px;
box-shadow:0 2px 8px rgba(0,0,0,0.06)}
.tx-icon-text{display:inline-block;
margin:8px 15px 8px 0}
h2{color:#2c3e50;
padding:15px 20px;
background:linear-gradient(120deg,#e3f2fd 0%,#bbdefb 100%);
border-radius:10px;
margin:40px 0 25px 0;
border-left:6px solid #2196f3}
h3{color:#34495e;
padding:12px 18px;
background:linear-gradient(120deg,#f3e5f5 0%,#e1bee7 100%);
border-radius:8px;
margin:30px 0 20px 0;
border-left:5px solid #9c27b0}
h4{color:#455a64;
margin:25px 0 15px 0;
padding-left:15px;
border-left:4px solid #90a4ae}</style>
        </head>
        <body>
            <h2>๐Ÿ“Š ์˜ค๋Š˜์˜ ํˆฌ์ž ๋ฆฌํฌํŠธ ์š”์•ฝ</h2>
            <p>์ด {len(results)}๊ฐœ์˜ ๋ฆฌํฌํŠธ๊ฐ€ ๋ถ„์„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</p>
        """
        
        for result in results:
            analysis = result['analysis']
            metrics = analysis.get('metrics', {})
            opinion = analysis.get('opinion', {})
            
            html += f"""
            <div class="report">
                <div class="title">{result['filename']}</div>
                <p><strong>ํˆฌ์ž์˜๊ฒฌ:</strong> {opinion.get('opinion_kr', 'N/A')}</p>
                <p><strong>๋ชฉํ‘œ์ฃผ๊ฐ€:</strong> <span class="metric">{metrics.get('target_price', 'N/A'):,}์›</span></p>
                <p><strong>์ƒ์Šน์—ฌ๋ ฅ:</strong> <span class="metric">{metrics.get('upside_potential', 'N/A')}%</span></p>
                <div class="summary">
                    <strong>์š”์•ฝ:</strong><br>
                    {result['summary'][:200]}...
                </div>
            </div>
            """
        
        html += """
        </body>
        </html>
        """
        
        return html
    
    def daily_job(self):
        """๋งค์ผ ์‹คํ–‰๋  ์ž‘์—…"""
        print(f"\n{'='*50}")
        print(f"๐Ÿค– ์ž๋™ ๋ถ„์„ ์‹œ์ž‘: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        print(f"{'='*50}\n")
        
        # 1. ์ƒˆ ๋ฆฌํฌํŠธ ์ˆ˜์ง‘
        new_reports = self.collect_new_reports()
        print(f"๐Ÿ“ฅ ์ƒˆ๋กœ์šด ๋ฆฌํฌํŠธ {len(new_reports)}๊ฐœ ๋ฐœ๊ฒฌ")
        
        # 2. ๊ฐ ๋ฆฌํฌํŠธ ์ฒ˜๋ฆฌ
        results = []
        for report_path in new_reports:
            try:
                result = self.process_report(report_path)
                results.append(result)
                self.reports_processed += 1
            except Exception as e:
                print(f"โŒ ์ฒ˜๋ฆฌ ์‹คํŒจ ({report_path}): {e}")
        
        # 3. ๊ฒฐ๊ณผ ์ด๋ฉ”์ผ ์ „์†ก
        if results:
            self.send_email_report(results)
        
        print(f"\nโœ… ์ž‘์—… ์™„๋ฃŒ! (์ด ์ฒ˜๋ฆฌ: {self.reports_processed}๊ฐœ)")
    
    def start_scheduler(self):
        """์Šค์ผ€์ค„๋Ÿฌ ์‹œ์ž‘"""
        # ๋งค์ผ ์˜ค์ „ 9์‹œ์— ์‹คํ–‰
        schedule.every().day.at("09:00").do(self.daily_job)
        
        # ํ…Œ์ŠคํŠธ์šฉ: ๋งค 1๋ถ„๋งˆ๋‹ค ์‹คํ–‰
        # schedule.every(1).minutes.do(self.daily_job)
        
        print("๐Ÿš€ ์Šค์ผ€์ค„๋Ÿฌ ์‹œ์ž‘!")
        print("๋งค์ผ ์˜ค์ „ 9์‹œ์— ์ž๋™์œผ๋กœ ๋ฆฌํฌํŠธ๋ฅผ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.")
        
        while True:
            schedule.run_pending()
            time.sleep(60)  # 1๋ถ„๋งˆ๋‹ค ์ฒดํฌ

# ์‚ฌ์šฉ ์˜ˆ์‹œ
email_config = {
    'from': 'your-email@gmail.com',
    'to': 'recipient@gmail.com',
    'username': 'your-email@gmail.com',
    'password': 'your-app-password'  # Gmail ์•ฑ ๋น„๋ฐ€๋ฒˆํ˜ธ
}

analyzer = AutomatedReportAnalyzer(email_config)
# analyzer.start_scheduler()  # ์‹ค์ œ ์šด์˜ ์‹œ ์ฃผ์„ ํ•ด์ œ

๐Ÿ” ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ ์ด๋ฉ”์ผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋‚˜ API ํ‚ค ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ์ง์ ‘ ๋„ฃ์œผ๋ฉด ์•ˆ ๋ผ!

์•ˆ์ „ํ•œ ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•:
โ€ข ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์‚ฌ์šฉ (os.environ.get('API_KEY'))
โ€ข .env ํŒŒ์ผ + python-dotenv ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ€ข AWS Secrets Manager, Azure Key Vault ๊ฐ™์€ ์„œ๋น„์Šค
โ€ข .gitignore์— ๋ฏผ๊ฐ ์ •๋ณด ํŒŒ์ผ ์ถ”๊ฐ€

ํŠนํžˆ GitHub์— ์˜ฌ๋ฆด ๋•Œ ์กฐ์‹ฌํ•ด์•ผ ํ•ด! ๐Ÿšจ

๐Ÿ“Š ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

์ด๋ก ๊ณผ ์ฝ”๋“œ๋Š” ์ถฉ๋ถ„ํžˆ ๋ดค์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ์‚ฌ๋ก€๋ฅผ ์‚ดํŽด๋ณด์ž! ๐Ÿ’ผ

์‚ฌ๋ก€ 1: ๊ฐœ์ธ ํˆฌ์ž์ž A์”จ์˜ ํ•˜๋ฃจ

Before (AI ๋„์ž… ์ „) ๐Ÿ˜ฐ ์˜ค์ „ 7์‹œ: ์ถœ๊ทผ ์ค€๋น„ํ•˜๋ฉด์„œ ์ฆ๊ถŒ์‚ฌ ์•ฑ 5๊ฐœ ํ™•์ธ
์˜ค์ „ 8์‹œ: ์ง€ํ•˜์ฒ ์—์„œ ๋ฆฌํฌํŠธ 3๊ฐœ ๋‹ค์šด๋กœ๋“œ, ์ œ๋ชฉ๋งŒ ํ™•์ธ
์ ์‹ฌ์‹œ๊ฐ„: ๋ฆฌํฌํŠธ 1๊ฐœ ์ฝ๊ธฐ ์‹œ์ž‘, 20ํŽ˜์ด์ง€ ์ค‘ 5ํŽ˜์ด์ง€ ์ฝ๊ณ  ํฌ๊ธฐ
ํ‡ด๊ทผ ํ›„: ํ”ผ๊ณคํ•ด์„œ ํˆฌ์ž ๊ณต๋ถ€๋Š” ๋‹ค์Œ์œผ๋กœ ๋ฏธ๋ฃธ
์ฃผ๋ง: ๋ฐ€๋ฆฐ ๋ฆฌํฌํŠธ 10๊ฐœ... ๊ฒฐ๊ตญ ๋Œ€์ถฉ ํ›‘์–ด๋ณด๊ธฐ๋งŒ

๊ฒฐ๊ณผ: ์ค‘์š”ํ•œ ํˆฌ์ž ๊ธฐํšŒ ๋†“์นจ, ์ŠคํŠธ๋ ˆ์Šค ์ฆ๊ฐ€ ๐Ÿ“‰

After (AI ๋„์ž… ํ›„) ๐Ÿ˜Š

์˜ค์ „ 7์‹œ: ์ผ์–ด๋‚˜์ž๋งˆ์ž ์ด๋ฉ”์ผ ํ™•์ธ
โ†’ AI๊ฐ€ ๋ฐค์ƒˆ ๋ถ„์„ํ•œ 5๊ฐœ ๋ฆฌํฌํŠธ ์š”์•ฝ๋ณธ ๋„์ฐฉ!
์˜ค์ „ 7์‹œ 10๋ถ„: ํ•ต์‹ฌ ๋‚ด์šฉ๋งŒ 5๋ถ„ ๋งŒ์— ํŒŒ์•…
โ†’ "์‚ผ์„ฑ์ „์ž ๋ชฉํ‘œ์ฃผ๊ฐ€ ์ƒํ–ฅ, ์ƒ์Šน์—ฌ๋ ฅ 15%"
โ†’ "SKํ•˜์ด๋‹‰์Šค ์‹ค์  ๊ฐœ์„  ์ „๋ง, ๋งค์ˆ˜ ์˜๊ฒฌ ์œ ์ง€"
์˜ค์ „ 7์‹œ 15๋ถ„: ๊ด€์‹ฌ ์ข…๋ชฉ 2๊ฐœ ์„ ์ •, ์ถ”๊ฐ€ ์กฐ์‚ฌ ๊ณ„ํš
์ ์‹ฌ์‹œ๊ฐ„: ์„ ํƒํ•œ ์ข…๋ชฉ๋งŒ ์ƒ์„ธ ๋ฆฌํฌํŠธ ํ™•์ธ
ํ‡ด๊ทผ ํ›„: ์—ฌ์œ ๋กญ๊ฒŒ ํˆฌ์ž ์ „๋žต ์ˆ˜๋ฆฝ

๊ฒฐ๊ณผ: ์‹œ๊ฐ„ ์ ˆ์•ฝ 90%, ํˆฌ์ž ์„ฑ๊ณผ ํ–ฅ์ƒ, ์ŠคํŠธ๋ ˆ์Šค ๊ฐ์†Œ! ๐Ÿ“ˆโœจ

์‚ฌ๋ก€ 2: ์ค‘์†Œ ์ž์‚ฐ์šด์šฉ์‚ฌ B์‚ฌ

๋„์ž… ๋ฐฐ๊ฒฝ: โ€ข ์• ๋„๋ฆฌ์ŠคํŠธ 3๋ช…์ด ํ•˜๋ฃจ ํ‰๊ท  30๊ฐœ ๋ฆฌํฌํŠธ ๊ฒ€ํ† 
โ€ข ์ธ๋ ฅ ๋ถ€์กฑ์œผ๋กœ ์ค‘์š”ํ•œ ์ •๋ณด ๋†“์น˜๋Š” ๊ฒฝ์šฐ ๋ฐœ์ƒ
โ€ข ๋ฆฌํฌํŠธ ์ •๋ฆฌ์—๋งŒ ํ•˜๋ฃจ 2~3์‹œ๊ฐ„ ์†Œ์š”

AI ์‹œ์Šคํ…œ ๋„์ž… ํ›„:
โ€ข ํ•˜๋ฃจ 100๊ฐœ ์ด์ƒ ๋ฆฌํฌํŠธ ์ž๋™ ๋ถ„์„
โ€ข ํ•ต์‹ฌ ์ •๋ณด๋งŒ ์ถ”์ถœํ•˜์—ฌ ๋Œ€์‹œ๋ณด๋“œ์— ํ‘œ์‹œ
โ€ข ์• ๋„๋ฆฌ์ŠคํŠธ๋“ค์€ ๋ถ„์„๊ณผ ์˜์‚ฌ๊ฒฐ์ •์— ์ง‘์ค‘
โ€ข ๋ฆฌํฌํŠธ ๊ฒ€ํ†  ์‹œ๊ฐ„ 70% ๋‹จ์ถ•

์ถ”๊ฐ€ ๊ธฐ๋Šฅ:
โ€ข ์ข…๋ชฉ๋ณ„ ๋ฆฌํฌํŠธ ํžˆ์Šคํ† ๋ฆฌ ์ž๋™ ๊ด€๋ฆฌ
โ€ข ๋ชฉํ‘œ์ฃผ๊ฐ€ ๋ณ€๋™ ์ถ”์ด ๊ทธ๋ž˜ํ”„ ์ž๋™ ์ƒ์„ฑ
โ€ข ํˆฌ์ž์˜๊ฒฌ ๋ณ€๊ฒฝ ์‹œ ์•Œ๋ฆผ ๋ฐœ์†ก
โ€ข ๊ฒฝ์Ÿ์‚ฌ ๋ฆฌํฌํŠธ์™€ ์ž๋™ ๋น„๊ต ๋ถ„์„

ROI: 6๊ฐœ์›” ๋งŒ์— ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋น„์šฉ ํšŒ์ˆ˜! ๐Ÿ’ฐ

์‚ฌ๋ก€ 3: ์žฌ๋Šฅ๋„ท์—์„œ ์„œ๋น„์Šค๋กœ ์ œ๊ณต

์ด๋Ÿฐ AI ๋ถ„์„ ์‹œ์Šคํ…œ์„ ์ง์ ‘ ๋งŒ๋“ค๊ธฐ ์–ด๋ ต๋‹ค๋ฉด? ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

์žฌ๋Šฅ๋„ท์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ๋Š” ์„œ๋น„์Šค:
โ€ข AI ํˆฌ์ž ๋ฆฌํฌํŠธ ๋ถ„์„ ์‹œ์Šคํ…œ ๊ฐœ๋ฐœ
โ€ข ๋งž์ถคํ˜• ์ž๋™ํ™” ์Šคํฌ๋ฆฝํŠธ ์ œ์ž‘
โ€ข ํฌ๋กค๋ง ๋ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ž๋™ํ™”
โ€ข ํˆฌ์ž ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์ถ•
โ€ข API ์—ฐ๋™ ๋ฐ ์‹œ์Šคํ…œ ํ†ตํ•ฉ

์‹ค์ œ๋กœ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์žฌ๋Šฅ๋„ท์—์„œ ์ด๋Ÿฐ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์–ด. ํ˜ผ์ž ๊ณ ๋ฏผํ•˜์ง€ ๋ง๊ณ  ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ๐Ÿ’ช

๐ŸŽจ ๋Œ€์‹œ๋ณด๋“œ ๋งŒ๋“ค๊ธฐ

๋ถ„์„ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๊ธฐ ์ข‹๊ฒŒ ์‹œ๊ฐํ™”ํ•˜๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•ด! ๊ฐ„๋‹จํ•œ ์›น ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ๐Ÿ–ฅ๏ธ

Streamlit์œผ๋กœ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“ค๊ธฐ

import streamlit as st
import pandas as pd
import plotly.graph_objects as go
from datetime import datetime, timedelta

# ํŽ˜์ด์ง€ ์„ค์ •
st.set_page_config(
    page_title="ํˆฌ์ž ๋ฆฌํฌํŠธ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ",
    page_icon="๐Ÿ“Š",
    layout="wide"
)

# ํƒ€์ดํ‹€
st.title("๐Ÿ“Š AI ํˆฌ์ž ๋ฆฌํฌํŠธ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ")
st.markdown("---")

# ์‚ฌ์ด๋“œ๋ฐ”
with st.sidebar:
    st.header("โš™๏ธ ์„ค์ •")
    
    # ๋‚ ์งœ ๋ฒ”์œ„ ์„ ํƒ
    date_range = st.date_input(
        "๋ถ„์„ ๊ธฐ๊ฐ„",
        value=(datetime.now() - timedelta(days=7), datetime.now())
    )
    
    # ์ข…๋ชฉ ์„ ํƒ
    stocks = st.multiselect(
        "์ข…๋ชฉ ์„ ํƒ",
        ["์‚ผ์„ฑ์ „์ž", "SKํ•˜์ด๋‹‰์Šค", "NAVER", "์นด์นด์˜ค", "LG์—๋„ˆ์ง€์†”๋ฃจ์…˜"],
        default=["์‚ผ์„ฑ์ „์ž", "SKํ•˜์ด๋‹‰์Šค"]
    )
    
    # ํˆฌ์ž์˜๊ฒฌ ํ•„ํ„ฐ
    opinions = st.multiselect(
        "ํˆฌ์ž์˜๊ฒฌ",
        ["๋งค์ˆ˜", "์ค‘๋ฆฝ", "๋งค๋„"],
        default=["๋งค์ˆ˜", "์ค‘๋ฆฝ"]
    )

# ๋ฉ”์ธ ์ปจํ…์ธ 
col1, col2, col3 = st.columns(3)

with col1:
    st.metric(
        label="๐Ÿ“„ ๋ถ„์„๋œ ๋ฆฌํฌํŠธ",
        value="127",
        delta="15"
    )

with col2:
    st.metric(
        label="๐ŸŽฏ ํ‰๊ท  ๋ชฉํ‘œ์ฃผ๊ฐ€ ์ƒ์Šน๋ฅ ",
        value="12.5%",
        delta="2.3%"
    )

with col3:
    st.metric(
        label="๐Ÿ“ˆ ๋งค์ˆ˜ ์˜๊ฒฌ ๋น„์œจ",
        value="68%",
        delta="5%"
    )

st.markdown("---")

# ๋ฆฌํฌํŠธ ๋ชฉ๋ก
st.subheader("๐Ÿ“‹ ์ตœ๊ทผ ๋ถ„์„ ๋ฆฌํฌํŠธ")

# ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ
data = {
    '์ข…๋ชฉ': ['์‚ผ์„ฑ์ „์ž', 'SKํ•˜์ด๋‹‰์Šค', 'NAVER', '์นด์นด์˜ค'],
    '์ฆ๊ถŒ์‚ฌ': ['ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ', 'NHํˆฌ์ž์ฆ๊ถŒ', '๋ฏธ๋ž˜์—์…‹์ฆ๊ถŒ', 'KB์ฆ๊ถŒ'],
    'ํˆฌ์ž์˜๊ฒฌ': ['๋งค์ˆ˜', '๋งค์ˆ˜', '์ค‘๋ฆฝ', '๋งค์ˆ˜'],
    '๋ชฉํ‘œ์ฃผ๊ฐ€': ['85,000์›', '145,000์›', '250,000์›', '65,000์›'],
    '์ƒ์Šน์—ฌ๋ ฅ': ['+15.2%', '+18.5%', '+8.3%', '+12.7%'],
    '๋ฐœํ–‰์ผ': ['2024-01-15', '2024-01-15', '2024-01-14', '2024-01-14']
}

df = pd.DataFrame(data)

# ์Šคํƒ€์ผ ์ ์šฉ
def highlight_opinion(val):
    if val == '๋งค์ˆ˜':
        return 'background-color: #d4edda; color: #155724'
    elif val == '๋งค๋„':
        return 'background-color: #f8d7da; color: #721c24'
    else:
        return 'background-color: #fff3cd; color: #856404'

styled_df = df.style.applymap(
    highlight_opinion,
    subset=['ํˆฌ์ž์˜๊ฒฌ']
)

st.dataframe(styled_df, use_container_width=True)

# ์ฐจํŠธ
st.markdown("---")
st.subheader("๐Ÿ“ˆ ๋ชฉํ‘œ์ฃผ๊ฐ€ ์ถ”์ด")

# ์ƒ˜ํ”Œ ์ฐจํŠธ ๋ฐ์ดํ„ฐ
chart_data = pd.DataFrame({
    '๋‚ ์งœ': pd.date_range(start='2024-01-01', periods=15, freq='D'),
    '์‚ผ์„ฑ์ „์ž': [75000, 76000, 77000, 78000, 79000, 80000, 81000, 
                82000, 81500, 82500, 83000, 83500, 84000, 84500, 85000],
    'SKํ•˜์ด๋‹‰์Šค': [120000, 122000, 125000, 127000, 130000, 132000, 135000,
                  137000, 138000, 140000, 141000, 142000, 143000, 144000, 145000]
})

fig = go.Figure()

for stock in ['์‚ผ์„ฑ์ „์ž', 'SKํ•˜์ด๋‹‰์Šค']:
    fig.add_trace(go.Scatter(
        x=chart_data['๋‚ ์งœ'],
        y=chart_data[stock],
        mode='lines+markers',
        name=stock,
        line=dict(width=3)
    ))

fig.update_layout(
    title="์ข…๋ชฉ๋ณ„ ๋ชฉํ‘œ์ฃผ๊ฐ€ ๋ณ€๋™ ์ถ”์ด",
    xaxis_title="๋‚ ์งœ",
    yaxis_title="๋ชฉํ‘œ์ฃผ๊ฐ€ (์›)",
    hovermode='x unified',
    height=400
)

st.plotly_chart(fig, use_container_width=True)

# ์š”์•ฝ ์„น์…˜
st.markdown("---")
st.subheader("๐Ÿ’ก AI ์š”์•ฝ")

with st.expander("์‚ผ์„ฑ์ „์ž - ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ (2024-01-15)", expanded=True):
    st.markdown("""
    **ํˆฌ์ž์˜๊ฒฌ:** ๋งค์ˆ˜ (์œ ์ง€)  
    **๋ชฉํ‘œ์ฃผ๊ฐ€:** 85,000์› (์ƒํ–ฅ)  
    
    **ํ•ต์‹ฌ ํฌ์ธํŠธ:**
    1. ๋ฉ”๋ชจ๋ฆฌ ๋ฐ˜๋„์ฒด ๊ฐ€๊ฒฉ ์ƒ์Šน์„ธ ์ง€์† ์ „๋ง
    2. 2024๋…„ 1๋ถ„๊ธฐ ์˜์—…์ด์ต 4.5์กฐ์› ์˜ˆ์ƒ (์ „๋…„ ๋Œ€๋น„ 300% ์ฆ๊ฐ€)
    3. HBM3 ์ˆ˜์š” ์ฆ๊ฐ€๋กœ ๊ณ ๋ถ€๊ฐ€ ์ œํ’ˆ ๋น„์ค‘ ํ™•๋Œ€
    
    **๋ฆฌ์Šคํฌ:**
    - ์ค‘๊ตญ ๊ฒฝ๊ธฐ ๋‘”ํ™”์— ๋”ฐ๋ฅธ ์ˆ˜์š” ๊ฐ์†Œ ๊ฐ€๋Šฅ์„ฑ
    - ํ™˜์œจ ๋ณ€๋™์„ฑ
    """)

# ํ‘ธํ„ฐ
st.markdown("---")
st.caption("๐Ÿค– Powered by AI Report Analyzer | ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ: " + 
          datetime.now().strftime("%Y-%m-%d %H:%M:%S"))

์ด ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์ด๋Ÿฐ ๋ฉ‹์ง„ ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ๋งŒ๋“ค์–ด์ ธ:

โ€ข ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜๋Š” ๋ฆฌํฌํŠธ ๋ชฉ๋ก
โ€ข ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•œ ์ฐจํŠธ์™€ ๊ทธ๋ž˜ํ”„
โ€ข ํ•„ํ„ฐ๋ง๊ณผ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ
โ€ข ๋ชจ๋ฐ”์ผ์—์„œ๋„ ์ž˜ ๋ณด์ด๋Š” ๋ฐ˜์‘ํ˜• ๋””์ž์ธ

Streamlit์€ ์ •๋ง ๊ฐ„๋‹จํ•˜๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•ด. Python๋งŒ ์•Œ๋ฉด ๋ˆ„๊ตฌ๋‚˜ ๋ฉ‹์ง„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŽจ

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด ์ด์ œ ๋” ๋น ๋ฅด๊ณ  ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“ค์–ด์•ผ์ง€! ์‹ค์ „์—์„œ ๊ผญ ํ•„์š”ํ•œ ์ตœ์ ํ™” ๊ธฐ๋ฒ•๋“ค์„ ์•Œ๋ ค์ค„๊ฒŒ. ๐Ÿš€

1. ์บ์‹ฑ ํ™œ์šฉํ•˜๊ธฐ

import hashlib
import json
from functools import lru_cache
import redis

class ReportCache:
    """๋ฆฌํฌํŠธ ๋ถ„์„ ๊ฒฐ๊ณผ ์บ์‹ฑ"""
    
    def __init__(self, use_redis=False):
        self.use_redis = use_redis
        if use_redis:
            self.redis_client = redis.Redis(
                host='localhost',
                port=6379,
                decode_responses=True
            )
    
    def get_cache_key(self, text: str) -> str:
        """ํ…์ŠคํŠธ์˜ ํ•ด์‹œ๊ฐ’์œผ๋กœ ์บ์‹œ ํ‚ค ์ƒ์„ฑ"""
        return hashlib.md5(text.encode()).hexdigest()
    
    def get(self, text: str):
        """์บ์‹œ์—์„œ ๊ฒฐ๊ณผ ๊ฐ€์ ธ์˜ค๊ธฐ"""
        key = self.get_cache_key(text)
        
        if self.use_redis:
            cached = self.redis_client.get(key)
            if cached:
                return json.loads(cached)
        else:
            # ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ (๊ฐ„๋‹จํ•œ ๋”•์…”๋„ˆ๋ฆฌ)
            if hasattr(self, '_cache') and key in self._cache:
                return self._cache[key]
        
        return None
    
    def set(self, text: str, result: dict, expire: int = 86400):
        """๊ฒฐ๊ณผ๋ฅผ ์บ์‹œ์— ์ €์žฅ (๊ธฐ๋ณธ 24์‹œ๊ฐ„)"""
        key = self.get_cache_key(text)
        
        if self.use_redis:
            self.redis_client.setex(
                key,
                expire,
                json.dumps(result)
            )
        else:
            if not hasattr(self, '_cache'):
                self._cache = {}
            self._cache[key] = result

# ์‚ฌ์šฉ ์˜ˆ์‹œ
cache = ReportCache(use_redis=False)

def analyze_with_cache(text: str):
    """์บ์‹œ๋ฅผ ํ™œ์šฉํ•œ ๋ถ„์„"""
    # ์บ์‹œ ํ™•์ธ
    cached_result = cache.get(text)
    if cached_result:
        print("โœ… ์บ์‹œ์—์„œ ๊ฒฐ๊ณผ ๋กœ๋“œ!")
        return cached_result
    
    # ์บ์‹œ ์—†์œผ๋ฉด ์ƒˆ๋กœ ๋ถ„์„
    print("๐Ÿ”„ ์ƒˆ๋กœ ๋ถ„์„ ์ค‘...")
    result = summarize_with_gpt(text)
    
    # ๊ฒฐ๊ณผ ์บ์‹ฑ
    cache.set(text, result)
    
    return result

๐Ÿ’ก ์บ์‹ฑ ํšจ๊ณผ: ๊ฐ™์€ ๋ฆฌํฌํŠธ๋ฅผ ๋‹ค์‹œ ๋ถ„์„ํ•  ๋•Œ:
โ€ข ๋ถ„์„ ์‹œ๊ฐ„: 10์ดˆ โ†’ 0.01์ดˆ (1000๋ฐฐ ๋น ๋ฆ„!)
โ€ข API ๋น„์šฉ: $0.05 โ†’ $0 (100% ์ ˆ๊ฐ!)
โ€ข ์„œ๋ฒ„ ๋ถ€ํ•˜: ๋Œ€ํญ ๊ฐ์†Œ

ํŠนํžˆ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ™์€ ๋ฆฌํฌํŠธ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ ์—„์ฒญ๋‚œ ํšจ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด!

2. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ

from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List
import time

def batch_process_reports(report_paths: List[str], max_workers: int = 5):
    """์—ฌ๋Ÿฌ ๋ฆฌํฌํŠธ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌ"""
    results = []
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        # ๋ชจ๋“  ์ž‘์—… ์ œ์ถœ
        future_to_path = {
            executor.submit(process_single_report, path): path 
            for path in report_paths
        }
        
        # ์™„๋ฃŒ๋œ ์ž‘์—…๋ถ€ํ„ฐ ๊ฒฐ๊ณผ ์ˆ˜์ง‘
        for future in as_completed(future_to_path):
            path = future_to_path[future]
            try:
                result = future.result()
                results.append(result)
                print(f"โœ… ์™„๋ฃŒ: {path}")
            except Exception as e:
                print(f"โŒ ์‹คํŒจ: {path} - {e}")
    
    return results

def process_single_report(path: str):
    """๋‹จ์ผ ๋ฆฌํฌํŠธ ์ฒ˜๋ฆฌ"""
    text = extract_text_from_pdf(path)
    summary = summarize_with_gpt(text)
    return {
        'path': path,
        'summary': summary,
        'processed_at': time.time()
    }

# ์‚ฌ์šฉ ์˜ˆ์‹œ
report_files = [
    "report1.pdf",
    "report2.pdf",
    "report3.pdf",
    "report4.pdf",
    "report5.pdf"
]

start_time = time.time()
results = batch_process_reports(report_files, max_workers=3)
end_time = time.time()

print(f"\n์ด ์ฒ˜๋ฆฌ ์‹œ๊ฐ„: {end_time - start_time:.2f}์ดˆ")
print(f"๋ฆฌํฌํŠธ๋‹น ํ‰๊ท : {(end_time - start_time) / len(report_files):.2f}์ดˆ")

3. ํ…์ŠคํŠธ ์••์ถ• ๋ฐ ์ฒญํ‚น

๊ธด ๋ฆฌํฌํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ๋Š” ํ…์ŠคํŠธ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๋‚˜๋ˆ„๋Š” ๊ฒŒ ์ค‘์š”ํ•ด!

def smart_chunk_text(text: str, max_chunk_size: int = 3000) -> List[str]:
    """ํ…์ŠคํŠธ๋ฅผ ์˜๋ฏธ ๋‹จ์œ„๋กœ ์ฒญํ‚น"""
    
    # ๋ฌธ๋‹จ ๋‹จ์œ„๋กœ ๋ถ„๋ฆฌ
    paragraphs = text.split('\n\n')
    
    chunks = []
    current_chunk = ""
    
    for para in paragraphs:
        # ํ˜„์žฌ ์ฒญํฌ์— ์ถ”๊ฐ€ํ•ด๋„ ํฌ๊ธฐ๊ฐ€ ๊ดœ์ฐฎ์œผ๋ฉด
        if len(current_chunk) + len(para) < max_chunk_size:
            current_chunk += para + "\n\n"
        else:
            # ํ˜„์žฌ ์ฒญํฌ ์ €์žฅํ•˜๊ณ  ์ƒˆ ์ฒญํฌ ์‹œ์ž‘
            if current_chunk:
                chunks.append(current_chunk.strip())
            current_chunk = para + "\n\n"
    
    # ๋งˆ์ง€๋ง‰ ์ฒญํฌ ์ถ”๊ฐ€
    if current_chunk:
        chunks.append(current_chunk.strip())
    
    return chunks

def summarize_long_report(text: str) -> str:
    """๊ธด ๋ฆฌํฌํŠธ๋ฅผ ์ฒญํฌ๋ณ„๋กœ ์š”์•ฝ ํ›„ ํ†ตํ•ฉ"""
    
    # 1๋‹จ๊ณ„: ํ…์ŠคํŠธ๋ฅผ ์ฒญํฌ๋กœ ๋‚˜๋ˆ„๊ธฐ
    chunks = smart_chunk_text(text, max_chunk_size=3000)
    print(f"๐Ÿ“„ {len(chunks)}๊ฐœ ์ฒญํฌ๋กœ ๋ถ„ํ• ")
    
    # 2๋‹จ๊ณ„: ๊ฐ ์ฒญํฌ ์š”์•ฝ
    chunk_summaries = []
    for i, chunk in enumerate(chunks):
        print(f"์ฒ˜๋ฆฌ ์ค‘: ์ฒญํฌ {i+1}/{len(chunks)}")
        summary = summarize_with_gpt(chunk, max_length=200)
        chunk_summaries.append(summary['summary'])
    
    # 3๋‹จ๊ณ„: ์š”์•ฝ๋“ค์„ ๋‹ค์‹œ ํ†ตํ•ฉ ์š”์•ฝ
    combined = "\n\n".join(chunk_summaries)
    final_summary = summarize_with_gpt(combined, max_length=500)
    
    return final_summary['summary']

๐Ÿ”’ ๋ณด์•ˆ ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

์‹ค์ „์—์„œ๋Š” ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๋“ค์ด ๋งŽ์ด ๋ฐœ์ƒํ•ด. ๊ฒฌ๊ณ ํ•œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํ•„์ˆ˜ ์š”์†Œ๋“ค์„ ์•Œ์•„๋ณด์ž! ๐Ÿ›ก๏ธ

์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ๋กœ๊น…

import logging
from datetime import datetime
import traceback

# ๋กœ๊ฑฐ ์„ค์ •
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('report_analyzer.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger('ReportAnalyzer')

class RobustReportAnalyzer:
    """์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ•ํ™”๋œ ๋ถ„์„๊ธฐ"""
    
    def __init__(self):
        self.success_count = 0
        self.error_count = 0
        self.errors = []
    
    def safe_process(self, pdf_path: str, max_retries: int = 3):
        """์•ˆ์ „ํ•œ ๋ฆฌํฌํŠธ ์ฒ˜๋ฆฌ (์žฌ์‹œ๋„ ํฌํ•จ)"""
        
        for attempt in range(max_retries):
            try:
                logger.info(f"์ฒ˜๋ฆฌ ์‹œ์ž‘: {pdf_path} (์‹œ๋„ {attempt + 1}/{max_retries})")
                
                # 1. PDF ์ถ”์ถœ
                text = self._safe_extract_pdf(pdf_path)
                if not text:
                    raise ValueError("ํ…์ŠคํŠธ ์ถ”์ถœ ์‹คํŒจ")
                
                # 2. ์š”์•ฝ ์ƒ์„ฑ
                summary = self._safe_summarize(text)
                if not summary:
                    raise ValueError("์š”์•ฝ ์ƒ์„ฑ ์‹คํŒจ")
                
                # 3. ๋ถ„์„
                analysis = self._safe_analyze(text)
                
                self.success_count += 1
                logger.info(f"โœ… ์ฒ˜๋ฆฌ ์„ฑ๊ณต: {pdf_path}")
                
                return {
                    'success': True,
                    'path': pdf_path,
                    'summary': summary,
                    'analysis': analysis
                }
                
            except Exception as e:
                logger.error(f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ (์‹œ๋„ {attempt + 1}): {str(e)}")
                logger.error(traceback.format_exc())
                
                if attempt == max_retries - 1:
                    # ๋งˆ์ง€๋ง‰ ์‹œ๋„๋„ ์‹คํŒจ
                    self.error_count += 1
                    self.errors.append({
                        'path': pdf_path,
                        'error': str(e),
                        'timestamp': datetime.now().isoformat()
                    })
                    
                    return {
                        'success': False,
                        'path': pdf_path,
                        'error': str(e)
                    }
                
                # ์žฌ์‹œ๋„ ์ „ ๋Œ€๊ธฐ
                time.sleep(2 ** attempt)  # ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„
    
    def _safe_extract_pdf(self, pdf_path: str) -> str:
        """์•ˆ์ „ํ•œ PDF ์ถ”์ถœ"""
        try:
            return extract_text_from_pdf(pdf_path)
        except Exception as e:
            logger.error(f"PDF ์ถ”์ถœ ์‹คํŒจ: {e}")
            raise
    
    def _safe_summarize(self, text: str) -> str:
        """์•ˆ์ „ํ•œ ์š”์•ฝ ์ƒ์„ฑ"""
        try:
            # ํ…์ŠคํŠธ ๊ธธ์ด ์ฒดํฌ
            if len(text) < 100:
                raise ValueError("ํ…์ŠคํŠธ๊ฐ€ ๋„ˆ๋ฌด ์งง์Œ")
            
            # ์š”์•ฝ ์ƒ์„ฑ
            result = summarize_with_gpt(text)
            return result.get('summary', '')
            
        except Exception as e:
            logger.error(f"์š”์•ฝ ์ƒ์„ฑ ์‹คํŒจ: {e}")
            # ํด๋ฐฑ: ๊ฐ„๋‹จํ•œ ์ถ”์ถœ ์š”์•ฝ
            return self._fallback_summary(text)
    
    def _fallback_summary(self, text: str) -> str:
        """ํด๋ฐฑ ์š”์•ฝ (AI ์‹คํŒจ ์‹œ)"""
        # ์ฒซ 500์ž๋งŒ ๋ฐ˜ํ™˜
        return text[:500] + "..."
    
    def _safe_analyze(self, text: str):
        """์•ˆ์ „ํ•œ ๋ถ„์„"""
        try:
            analyzer = InvestmentReportAnalyzer(text)
            return analyzer.analyze()
        except Exception as e:
            logger.error(f"๋ถ„์„ ์‹คํŒจ: {e}")
            return {}
    
    def get_statistics(self):
        """์ฒ˜๋ฆฌ ํ†ต๊ณ„"""
        total = self.success_count + self.error_count
        success_rate = (self.success_count / total * 100) if total > 0 else 0
        
        return {
            'total': total,
            'success': self.success_count,
            'errors': self.error_count,
            'success_rate': f"{success_rate:.2f}%"
        }

๐Ÿ’ฐ ๋น„์šฉ ์ตœ์ ํ™” ์ „๋žต

AI API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋น„์šฉ์ด ๋ฐœ์ƒํ•ด. ๋˜‘๋˜‘ํ•˜๊ฒŒ ๋น„์šฉ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐Ÿ’ธ

๐Ÿ“Š ์‹ค์ œ ๋น„์šฉ ๋ถ„์„ (์›” 100๊ฐœ ๋ฆฌํฌํŠธ ๊ธฐ์ค€) GPT-4 ์‚ฌ์šฉ ์‹œ:
โ€ข ๋ฆฌํฌํŠธ๋‹น ํ‰๊ท  ํ† ํฐ: 5,000 (์ž…๋ ฅ) + 500 (์ถœ๋ ฅ)
โ€ข ๋ฆฌํฌํŠธ๋‹น ๋น„์šฉ: ์•ฝ $0.20
โ€ข ์›” ์ด ๋น„์šฉ: $20

GPT-3.5-turbo ์‚ฌ์šฉ ์‹œ:
โ€ข ๋ฆฌํฌํŠธ๋‹น ํ‰๊ท  ํ† ํฐ: 5,000 (์ž…๋ ฅ) + 500 (์ถœ๋ ฅ)
โ€ข ๋ฆฌํฌํŠธ๋‹น ๋น„์šฉ: ์•ฝ $0.02
โ€ข ์›” ์ด ๋น„์šฉ: $2 (90% ์ ˆ๊ฐ!)

์˜คํ”ˆ์†Œ์Šค ๋ชจ๋ธ (์ž์ฒด ์„œ๋ฒ„):
โ€ข API ๋น„์šฉ: $0
โ€ข ์„œ๋ฒ„ ๋น„์šฉ: ์›” $50~100 (GPU ์ธ์Šคํ„ด์Šค)
โ€ข ์›” 100๊ฐœ ์ด์ƒ ์ฒ˜๋ฆฌ ์‹œ ์œ ๋ฆฌ

๐Ÿ’ก ์ถ”์ฒœ ์ „๋žต:
โ€ข ์ผ๋ฐ˜ ์š”์•ฝ: GPT-3.5-turbo
โ€ข ์ค‘์š” ๋ฆฌํฌํŠธ: GPT-4
โ€ข ๋Œ€๋Ÿ‰ ์ฒ˜๋ฆฌ: ์˜คํ”ˆ์†Œ์Šค ๋ชจ๋ธ

ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋ง

class CostTracker:
    """API ๋น„์šฉ ์ถ”์ """
    
    def __init__(self):
        self.total_tokens = 0
        self.total_cost = 0
        self.requests = []
    
    def track_request(self, model: str, input_tokens: int, output_tokens: int):
        """์š”์ฒญ ์ถ”์ """
        
        # ๋ชจ๋ธ๋ณ„ ๊ฐ€๊ฒฉ (2024๋…„ ๊ธฐ์ค€, 1000 ํ† ํฐ๋‹น)
        prices = {
            'gpt-4': {'input': 0.03, 'output': 0.06},
            'gpt-3.5-turbo': {'input': 0.001, 'output': 0.002}
        }
        
        if model in prices:
            cost = (
                input_tokens / 1000 * prices[model]['input'] +
                output_tokens / 1000 * prices[model]['output']
            )
            
            self.total_tokens += (input_tokens + output_tokens)
            self.total_cost += cost
            
            self.requests.append({
                'timestamp': datetime.now(),
                'model': model,
                'tokens': input_tokens + output_tokens,
                'cost': cost
            })
    
    def get_summary(self):
        """๋น„์šฉ ์š”์•ฝ"""
        return {
            'total_requests': len(self.requests),
            'total_tokens': self.total_tokens,
            'total_cost': f"${self.total_cost:.2f}",
            'avg_cost_per_request': f"${self.total_cost / len(self.requests):.4f}" if self.requests else "$0"
        }
    
    def should_switch_model(self, threshold: float = 10.0) -> bool:
        """๋น„์šฉ์ด ์ž„๊ณ„๊ฐ’์„ ๋„˜์œผ๋ฉด ๋ชจ๋ธ ์ „ํ™˜ ์ œ์•ˆ"""
        return self.total_cost > threshold

# ์‚ฌ์šฉ ์˜ˆ์‹œ
tracker = CostTracker()

def summarize_with_tracking(text: str):
    """๋น„์šฉ ์ถ”์ ์ด ํฌํ•จ๋œ ์š”์•ฝ"""
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": text}]
    )
    
    # ๋น„์šฉ ์ถ”์ 
    tracker.track_request(
        model="gpt-3.5-turbo",
        input_tokens=response.usage.prompt_tokens,
        output_tokens=response.usage.completion_tokens
    )
    
    # ๋น„์šฉ ๊ฒฝ๊ณ 
    if tracker.should_switch_model(threshold=10.0):
        logger.warning("โš ๏ธ ์›” ๋น„์šฉ์ด $10๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค!")
    
    return response.choices[0].message.content

๐Ÿš€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ

๊ธฐ๋ณธ ๊ธฐ๋Šฅ์€ ๋‹ค ๋งŒ๋“ค์—ˆ์œผ๋‹ˆ, ์ด์ œ ๋” ๋ฉ‹์ง„ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž! โœจ

1. ๋ฆฌํฌํŠธ ๋น„๊ต ๋ถ„์„

๊ฐ™์€ ์ข…๋ชฉ์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ ์ฆ๊ถŒ์‚ฌ์˜ ๋ฆฌํฌํŠธ๋ฅผ ๋น„๊ตํ•˜๋ฉด ๋” ๊ฐ๊ด€์ ์ธ ํŒ๋‹จ์ด ๊ฐ€๋Šฅํ•ด!

def compare_reports(reports: List[Dict]) -> Dict:
    """์—ฌ๋Ÿฌ ๋ฆฌํฌํŠธ ๋น„๊ต ๋ถ„์„"""
    
    comparison = {
        'stock': reports[0]['stock_name'],
        'report_count': len(reports),
        'opinions': {},
        'target_prices': [],
        'consensus': {}
    }
    
    # ํˆฌ์ž์˜๊ฒฌ ์ง‘๊ณ„
    for report in reports:
        opinion = report.get('opinion', 'N/A')
        comparison['opinions'][opinion] = comparison['opinions'].get(opinion, 0) + 1
        
        target_price = report.get('target_price')
        if target_price:
            comparison['target_prices'].append({
                'firm': report['firm'],
                'price': target_price
            })
    
    # ์ปจ์„ผ์„œ์Šค ๊ณ„์‚ฐ
    if comparison['target_prices']:
        prices = [tp['price'] for tp in comparison['target_prices']]
        comparison['consensus'] = {
            'avg_target_price': sum(prices) / len(prices),
            'max_target_price': max(prices),
            'min_target_price': min(prices),
            'std_deviation': statistics.stdev(prices) if len(prices) > 1 else 0
        }
    
    # ๊ฐ€์žฅ ๋งŽ์€ ์˜๊ฒฌ
    if comparison['opinions']:
        comparison['majority_opinion'] = max(
            comparison['opinions'].items(),
            key=lambda x: x[1]
        )[0]
    
    return comparison

# ์‚ฌ์šฉ ์˜ˆ์‹œ
samsung_reports = [
    {'stock_name': '์‚ผ์„ฑ์ „์ž', 'firm': 'ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ', 'opinion': '๋งค์ˆ˜', 'target_price': 85000},
    {'stock_name': '์‚ผ์„ฑ์ „์ž', 'firm': 'NHํˆฌ์ž์ฆ๊ถŒ', 'opinion': '๋งค์ˆ˜', 'target_price': 87000},
    {'stock_name': '์‚ผ์„ฑ์ „์ž', 'firm': '๋ฏธ๋ž˜์—์…‹์ฆ๊ถŒ', 'opinion': '์ค‘๋ฆฝ', 'target_price': 80000},
]

comparison = compare_reports(samsung_reports)
print(f"์ปจ์„ผ์„œ์Šค ๋ชฉํ‘œ์ฃผ๊ฐ€: {comparison['consensus']['avg_target_price']:,.0f}์›")
print(f"๋‹ค์ˆ˜ ์˜๊ฒฌ: {comparison['majority_opinion']}")

2. ์•Œ๋ฆผ ์‹œ์Šคํ…œ

์ค‘์š”ํ•œ ๋ณ€ํ™”๊ฐ€ ์žˆ์„ ๋•Œ ์ฆ‰์‹œ ์•Œ๋ ค์ฃผ๋Š” ์‹œ์Šคํ…œ!

import requests

class AlertSystem:
    """์•Œ๋ฆผ ์‹œ์Šคํ…œ"""
    
    def __init__(self, slack_webhook: str = None, telegram_token: str = None):
        self.slack_webhook = slack_webhook
        self.telegram_token = telegram_token
    
    def should_alert(self, analysis: Dict) -> bool:
        """์•Œ๋ฆผ์ด ํ•„์š”ํ•œ์ง€ ํŒ๋‹จ"""
        
        # ์•Œ๋ฆผ ์กฐ๊ฑด๋“ค
        conditions = [
            # ๋ชฉํ‘œ์ฃผ๊ฐ€ ๋Œ€ํญ ์ƒํ–ฅ (10% ์ด์ƒ)
            analysis.get('target_price_change', 0) > 10,
            
            # ํˆฌ์ž์˜๊ฒฌ ๋ณ€๊ฒฝ
            analysis.get('opinion_changed', False),
            
            # ์ƒ์Šน์—ฌ๋ ฅ 20% ์ด์ƒ
            analysis.get('upside_potential', 0) > 20,
            
            # ์‹ค์  ์„œํ”„๋ผ์ด์ฆˆ
            'surprise' in analysis.get('keywords', [])
        ]
        
        return any(conditions)
    
    def send_slack(self, message: str):
        """์Šฌ๋ž™ ์•Œ๋ฆผ"""
        if not self.slack_webhook:
            return
        
        payload = {
            'text': message,
            'username': 'Report Analyzer Bot',
            'icon_emoji': ':chart_with_upwards_trend:'
        }
        
        try:
            response = requests.post(self.slack_webhook, json=payload)
            if response.status_code == 200:
                logger.info("โœ… ์Šฌ๋ž™ ์•Œ๋ฆผ ์ „์†ก ์™„๋ฃŒ")
        except Exception as e:
            logger.error(f"์Šฌ๋ž™ ์•Œ๋ฆผ ์‹คํŒจ: {e}")
    
    def send_telegram(self, message: str, chat_id: str):
        """ํ…”๋ ˆ๊ทธ๋žจ ์•Œ๋ฆผ"""
        if not self.telegram_token:
            return
        
        url = f"https://api.telegram.org/bot{self.telegram_token}/sendMessage"
        payload = {
            'chat_id': chat_id,
            'text': message,
            'parse_mode': 'Markdown'
        }
        
        try:
            response = requests.post(url, json=payload)
            if response.status_code == 200:
                logger.info("โœ… ํ…”๋ ˆ๊ทธ๋žจ ์•Œ๋ฆผ ์ „์†ก ์™„๋ฃŒ")
        except Exception as e:
            logger.error(f"ํ…”๋ ˆ๊ทธ๋žจ ์•Œ๋ฆผ ์‹คํŒจ: {e}")
    
    def format_alert_message(self, analysis: Dict) -> str:
        """์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ํฌ๋งท"""
        message = f"""
๐Ÿšจ *์ค‘์š” ๋ฆฌํฌํŠธ ์•Œ๋ฆผ*

๐Ÿ“Š ์ข…๋ชฉ: {analysis.get('stock_name', 'N/A')}
๐Ÿข ์ฆ๊ถŒ์‚ฌ: {analysis.get('firm', 'N/A')}

๐Ÿ’ฐ ๋ชฉํ‘œ์ฃผ๊ฐ€: {analysis.get('target_price', 'N/A'):,}์›
๐Ÿ“ˆ ์ƒ์Šน์—ฌ๋ ฅ: {analysis.get('upside_potential', 'N/A')}%
โญ ํˆฌ์ž์˜๊ฒฌ: {analysis.get('opinion', 'N/A')}

๐Ÿ“ ํ•ต์‹ฌ ์š”์•ฝ:
{analysis.get('summary', 'N/A')[:200]}...
        """
        return message.strip()

# ์‚ฌ์šฉ ์˜ˆ์‹œ
alert_system = AlertSystem(
    slack_webhook="your-slack-webhook-url",
    telegram_token="your-telegram-bot-token"
)

def process_with_alert(report_path: str):
    """์•Œ๋ฆผ ํฌํ•จ ์ฒ˜๋ฆฌ"""
    analysis = process_report(report_path)
    
    if alert_system.should_alert(analysis):
        message = alert_system.format_alert_message(analysis)
        alert_system.send_slack(message)
        alert_system.send_telegram(message, chat_id="your-chat-id")

๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์•ฑ ์—ฐ๋™

์ด์ œ ๋ชจ๋ฐ”์ผ์—์„œ๋„ ํŽธํ•˜๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿ“ฒ

REST API ๋งŒ๋“ค๊ธฐ

from flask import Flask, request, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก

# ์ „์—ญ ๋ถ„์„๊ธฐ
analyzer = RobustReportAnalyzer()

@app.route('/api/reports', methods=['GET'])
def get_reports():
    """๋ฆฌํฌํŠธ ๋ชฉ๋ก ์กฐํšŒ"""
    # ์‹ค์ œ๋กœ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒ
    reports = [
        {
            'id': 1,
            'stock': '์‚ผ์„ฑ์ „์ž',
            'firm': 'ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ',
            'date': '2024-01-15',
            'opinion': '๋งค์ˆ˜',
            'target_price': 85000
        },
        # ... ๋” ๋งŽ์€ ๋ฆฌํฌํŠธ
    ]
    
    return jsonify({
        'success': True,
        'data': reports,
        'count': len(reports)
    })

@app.route('/api/reports/<int:report_id>', methods=['GET'])
def get_report_detail(report_id):
    """๋ฆฌํฌํŠธ ์ƒ์„ธ ์กฐํšŒ"""
    # ์‹ค์ œ๋กœ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒ
    report = {
        'id': report_id,
        'stock': '์‚ผ์„ฑ์ „์ž',
        'firm': 'ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ',
        'date': '2024-01-15',
        'opinion': '๋งค์ˆ˜',
        'target_price': 85000,
        'summary': 'AI๊ฐ€ ์ƒ์„ฑํ•œ ์š”์•ฝ ๋‚ด์šฉ...',
        'key_points': [
            '๋ฉ”๋ชจ๋ฆฌ ๋ฐ˜๋„์ฒด ๊ฐ€๊ฒฉ ์ƒ์Šน',
            '1๋ถ„๊ธฐ ์‹ค์  ๊ฐœ์„  ์ „๋ง',
            'HBM3 ์ˆ˜์š” ์ฆ๊ฐ€'
        ]
    }
    
    return jsonify({
        'success': True,
        'data': report
    })

@app.route('/api/analyze', methods=['POST'])
def analyze_report():
    """์ƒˆ ๋ฆฌํฌํŠธ ๋ถ„์„"""
    
    # ํŒŒ์ผ ์—…๋กœ๋“œ ๋ฐ›๊ธฐ
    if 'file' not in request.files:
        return jsonify({
            'success': False,
            'error': 'ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค'
        }), 400
    
    file = request.files['file']
    
    # ์ž„์‹œ ์ €์žฅ
    temp_path = f"/tmp/{file.filename}"
    file.save(temp_path)
    
    # ๋ถ„์„ ์‹คํ–‰
    try:
        result = analyzer.safe_process(temp_path)
        
        return jsonify({
            'success': True,
            'data': result
        })
    except Exception as e:
        return jsonify({
            'success': False,
            'error': str(e)
        }), 500

@app.route('/api/search', methods=['GET'])
def search_reports():
    """๋ฆฌํฌํŠธ ๊ฒ€์ƒ‰"""
    query = request.args.get('q', '')
    stock = request.args.get('stock', '')
    
    # ๊ฒ€์ƒ‰ ๋กœ์ง (์‹ค์ œ๋กœ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ)
    results = []
    
    return jsonify({
        'success': True,
        'data': results,
        'query': query
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„

์—ฌ๊ธฐ๊นŒ์ง€ ์™”๋‹ค๋ฉด ์ •๋ง ๋Œ€๋‹จํ•ด! ๐ŸŽ‰ ์ด์ œ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ์ถ”์ฒœํ•ด:

๐Ÿ“š ์ถ”์ฒœ ํ•™์Šต ์ž๋ฃŒ: ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ (NLP):
โ€ข Hugging Face Transformers ๊ณต์‹ ๋ฌธ์„œ
โ€ข "Natural Language Processing with Transformers" ์ฑ…
โ€ข Stanford CS224N ๊ฐ•์˜

๋จธ์‹ ๋Ÿฌ๋‹/๋”ฅ๋Ÿฌ๋‹:
โ€ข Andrew Ng์˜ Machine Learning ๊ฐ•์˜
โ€ข Fast.ai ์‹ค์ „ ๋”ฅ๋Ÿฌ๋‹ ๊ฐ•์˜
โ€ข "Hands-On Machine Learning" ์ฑ…

๊ธˆ์œต ๋ฐ์ดํ„ฐ ๋ถ„์„:
โ€ข FinanceDataReader ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ€ข Quantitative Finance ๊ด€๋ จ ๋…ผ๋ฌธ๋“ค
โ€ข ์ฆ๊ถŒ์‚ฌ API ๋ฌธ์„œ (ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ, ํ‚ค์›€์ฆ๊ถŒ ๋“ฑ)

์›น ๊ฐœ๋ฐœ:
โ€ข Flask/FastAPI ๊ณต์‹ ๋ฌธ์„œ
โ€ข React Native (๋ชจ๋ฐ”์ผ ์•ฑ)
โ€ข Docker & Kubernetes (๋ฐฐํฌ)

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด

1. ์‹ค์‹œ๊ฐ„ ๋‰ด์Šค ๋ถ„์„ ์‹œ์Šคํ…œ โ€ข ๋„ค์ด๋ฒ„ ๋‰ด์Šค, ๊ตฌ๊ธ€ ๋‰ด์Šค ํฌ๋กค๋ง
โ€ข ์‹ค์‹œ๊ฐ„ ๊ฐ์„ฑ ๋ถ„์„
โ€ข ์ฃผ๊ฐ€ ์˜ํ–ฅ๋„ ์˜ˆ์ธก

2. ํฌํŠธํด๋ฆฌ์˜ค ์ถ”์ฒœ ์‹œ์Šคํ…œ
โ€ข ๋ฆฌํฌํŠธ ๋ถ„์„ ๊ฒฐ๊ณผ ๊ธฐ๋ฐ˜
โ€ข ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ ์•Œ๊ณ ๋ฆฌ์ฆ˜
โ€ข ๋ฐฑํ…Œ์ŠคํŒ… ๊ธฐ๋Šฅ

3. ์†Œ์…œ ํŠธ๋ ˆ์ด๋”ฉ ํ”Œ๋žซํผ
โ€ข ๋ถ„์„ ๊ฒฐ๊ณผ ๊ณต์œ 
โ€ข ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ๋Šฅ
โ€ข ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…

4. AI ํˆฌ์ž ์–ด์‹œ์Šคํ„ดํŠธ ์ฑ—๋ด‡
โ€ข ์นด์นด์˜คํ†ก/ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡
โ€ข ์Œ์„ฑ ์ธ์‹ ๊ธฐ๋Šฅ
โ€ข ๊ฐœ์ธํ™”๋œ ์ถ”์ฒœ

๐Ÿ’ก ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€์šฐ, ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! ๐ŸŽ‰

์šฐ๋ฆฌ๊ฐ€ ํ•จ๊ป˜ ์•Œ์•„๋ณธ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด๋ฉด:

โœ… AI๋กœ ํˆฌ์ž ๋ฆฌํฌํŠธ๋ฅผ ์ž๋™ ๋ถ„์„ํ•˜๋Š” ์ด์œ ์™€ ๋ฐฉ๋ฒ•
โœ… PDF ํŒŒ์‹ฑ๋ถ€ํ„ฐ ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ๊นŒ์ง€ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ
โœ… GPT API์™€ ์˜คํ”ˆ์†Œ์Šค ๋ชจ๋ธ ํ™œ์šฉ๋ฒ•
โœ… ์ž๋™ํ™” ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋ฐ ์Šค์ผ€์ค„๋ง
โœ… ๋Œ€์‹œ๋ณด๋“œ ์ œ์ž‘๊ณผ ์‹œ๊ฐํ™”
โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”์™€ ๋น„์šฉ ๊ด€๋ฆฌ
โœ… ์—๋Ÿฌ ์ฒ˜๋ฆฌ์™€ ๋ณด์•ˆ
โœ… ๋ชจ๋ฐ”์ผ ์•ฑ ์—ฐ๋™๊นŒ์ง€!

์ด ๋ชจ๋“  ๊ฑธ ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํ•˜๊ฒŒ ๋งŒ๋“ค ํ•„์š”๋Š” ์—†์–ด. ์ž‘์€ ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ ๊ฐœ์„ ํ•ด๋‚˜๊ฐ€๋ฉด ๋ผ! ๐Ÿ’ช

ํ˜น์‹œ ํ˜ผ์ž ํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ด๋Ÿฐ ํ”„๋กœ์ ํŠธ๋ฅผ ๋„์™€์ฃผ๊ณ  ์žˆ๊ฑฐ๋“ ! ๐Ÿค

ํˆฌ์ž๋Š” ํ•ญ์ƒ ์‹ ์ค‘ํ•˜๊ฒŒ, ๊ทธ๋ฆฌ๊ณ  AI๋Š” ๋„๊ตฌ์ผ ๋ฟ์ด๋ผ๋Š” ๊ฑธ ์žŠ์ง€ ๋งˆ! ์ตœ์ข… ํŒ๋‹จ์€ ํ•ญ์ƒ ์‚ฌ๋žŒ์ด ํ•ด์•ผ ํ•ด. ๐Ÿ˜Š

์ž, ์ด์ œ ๋‹น์‹ ๋„ AI ํˆฌ์ž ๋ฆฌํฌํŠธ ๋ถ„์„ ์ „๋ฌธ๊ฐ€๊ฐ€ ๋˜์—ˆ์–ด! ๐ŸŽ“โœจ

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋‹ค๊ฐ€ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“ ์ง€ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๋ฌผ์–ด๋ณด๊ฑฐ๋‚˜, ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„๋ด. ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•˜๋‹ˆ๊นŒ! ๐ŸŒฑ

ํ–‰์šด์„ ๋นŒ์–ด! ํ™”์ดํŒ…! ๐Ÿš€๐Ÿ’ฐ๐Ÿ“ˆ

AI ํˆฌ์ž ๋ฆฌํฌํŠธ ๋ถ„์„ ๋‹น์‹ ์˜ ํˆฌ์ž ํŒŒํŠธ๋„ˆ ๐Ÿค– AI ๋ถ„์„ โšก ์ž๋™ํ™” ๐Ÿ“Š ์‹œ๊ฐํ™” ๐Ÿš€ ์„ฑ๊ณต ์ง€๊ธˆ ๋ฐ”๋กœ ์‹œ์ž‘ํ•˜์„ธ์š”!
๋Œ“๊ธ€ ์ž‘์„ฑ

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

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

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