<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Aha-Log</title>
    <link>https://aha-log.tistory.com/</link>
    <description>aha-log 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 18 May 2026 18:31:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>aha0</managingEditor>
    <item>
      <title>Target</title>
      <link>https://aha-log.tistory.com/2</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpZACg/btsO5XAlJQw/UqmzD4mEeErDC53bsqPgw0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpZACg/btsO5XAlJQw/UqmzD4mEeErDC53bsqPgw0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpZACg/btsO5XAlJQw/UqmzD4mEeErDC53bsqPgw0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpZACg%2FbtsO5XAlJQw%2FUqmzD4mEeErDC53bsqPgw0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Plan/1-1 Vacation</category>
      <author>aha0</author>
      <guid isPermaLink="true">https://aha-log.tistory.com/2</guid>
      <comments>https://aha-log.tistory.com/2#entry2comment</comments>
      <pubDate>Sun, 6 Jul 2025 19:24:11 +0900</pubDate>
    </item>
    <item>
      <title>PRD Creater</title>
      <link>https://aha-log.tistory.com/1</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. PRD Bot (1차)&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%; height: 180px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: center; width: 15.4651%; height: 20px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: center; width: 84.4186%; height: 20px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center; width: 15.4651%; height: 20px;&quot;&gt;&lt;b&gt;프로젝트명&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 84.4186%; height: 20px;&quot;&gt;PRD Bot (Problem Random Defense)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center; width: 15.4651%; height: 20px;&quot;&gt;&lt;b&gt;개발 기간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9; text-align: left; width: 84.4186%; height: 20px;&quot;&gt;2025.05.19 ~ 2025.05.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center; width: 15.4651%; height: 20px;&quot;&gt;&lt;b&gt;협업 팀원&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 84.4186%; height: 20px;&quot;&gt;&lt;span&gt;JW&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center; width: 15.4651%; height: 60px;&quot;&gt;&lt;b&gt;개발 목적&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9; width: 84.4186%; height: 60px;&quot;&gt;Expotential 회사에서는 Problem Random Defense 연습셋을 매주 만들어 고객들에게 공급하고 있다. 하지만 해당 업무를 맡고 있는 담당자가 이를 자꾸 까먹는다는 문제가 발생했고, 이를 방지하기 위해 담당자에게 매주 이를 만들라고 알림을 주는 간단한 소프트웨어를 만들어보았다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center; width: 15.4651%; height: 40px;&quot;&gt;&lt;b&gt;개발 목표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 84.4186%; height: 40px;&quot;&gt;Expotential 회사는 업무를 보다 효과적으로 추진하기 위해 디스코드 서버를 사용하고 있다. 알림 메세지는 디스코드 서버에 디스코드 봇을 만들어 지정 채널에 멘션과 함께 발송하고자 한다.&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(1) Python 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751796048952&quot; class=&quot;python&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# (0) 전처리
import discord
from datetime import datetime               # 현재 요일 &amp;amp; 시간
from discord.ext import commands, tasks     # 봇 &amp;amp; 반복작업
import asyncio                              # 비동기 sleep

# (1) 기본설정
# - Intents : 수신하는 이벤트유형
intents = discord.Intents.all()
# - Bot(명령어, 이벤트) : 봇 생성
bot = commands.Bot(command_prefix='!', intents=intents)
# - 제어변수
today = False

# (2) 설정한 시간에 &quot;연습세트를 만들거라!&quot;라고 메세지를 보내는 코드
@tasks.loop(seconds=30)
async def send_message():
    global today
    # - 요일 &amp;amp; 날짜 변수
    now = datetime.now()
    # - 현재 요일 &amp;amp; 시간을 통해 전송
    if (now.weekday() == 4) and (now.hour == 12) and (now.minute == 00) and (today == False):               
        channel = bot.get_channel(1374977827417620492)
        await channel.send(&quot;연습세트를 만들거라!&quot;)
        today = True
    else:
        if now.weekday() != 4:
            today = False

# (3) 봇 연결 &amp;amp; 실행
# - 연결
@bot.event
async def on_ready():
    send_message.start()
# - 실행
bot.run(&quot;MTM3NDk3NzMxNjAyNjg0NzMwMw.GcwbDp.ikyOJ0vzsF4m33TqVtmSJFoQG9UFS6ThQnLW7w&quot;)​&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsfEwg/btsO5LmACnA/pPYSlp0Q5UJvUhtFTl0kN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsfEwg/btsO5LmACnA/pPYSlp0Q5UJvUhtFTl0kN0/img.png&quot; data-alt=&quot;실행결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsfEwg/btsO5LmACnA/pPYSlp0Q5UJvUhtFTl0kN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsfEwg%2FbtsO5LmACnA%2FpPYSlp0Q5UJvUhtFTl0kN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;100&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실행결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(2) JavaScript 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751796070065&quot; class=&quot;typescript&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// (0) 전처리
// - discord.js에서 사용하는 클래스 가져옴
const { Client, Events, GatewayIntentBits } = require('discord.js');
// - config.json에서 token을 가져옴 (보안상의 이유로 다른 파일로 분리시켜 모듈화하는 것이 좋음)
const { token } = require('./config.json');

// (1) 기본설정
// - 봇이 속해있는 채널에서 이벤트를 가져오도록 설정
const client = new Client({ intents: [
    GatewayIntentBits.Guilds,           // 서버 접속 확인
    GatewayIntentBits.GuildMessages     // 메세지 보내기 &amp;amp; 받기
]});
// - 제어변수
let today = false;

// (2) 설정한 시간에 &quot;연습세트를 만드세요!&quot;라고 메세지를 보내는 코드
// - 봇이 준비될 시, 한 번만 실행
client.once('ready', () =&amp;gt; {
    console.log(`  Bot is ready as ${client.user.tag}`);

    setInterval(async () =&amp;gt; {
        const now = new Date();

        if (now.getDay() == 5 &amp;amp;&amp;amp; now.getHours() == 12 &amp;amp;&amp;amp; now.getMinutes() == 0 &amp;amp;&amp;amp; today == false) {
            const channel = await client.channels.fetch('1374977827417620492');
            if (channel) {
                channel.send(&quot;연습세트를 만드세요!&quot;);
                today = true;
            }
        } else {
            if (now.getDay() != 5) today = false;
        }
    }, 30 * 1000);
});

// - 봇이 서버에 접속
client.login(token);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtGxvP/btsO60C5duJ/KMv6DP7x5ZTCGtyKF0GCQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtGxvP/btsO60C5duJ/KMv6DP7x5ZTCGtyKF0GCQ1/img.png&quot; data-alt=&quot;실행결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtGxvP/btsO60C5duJ/KMv6DP7x5ZTCGtyKF0GCQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtGxvP%2FbtsO60C5duJ%2FKMv6DP7x5ZTCGtyKF0GCQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;455&quot; height=&quot;83&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실행결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. PRD Creater (2차)&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%; height: 150px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: center; width: 13.9535%; height: 19px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: center; width: 85.9302%; height: 19px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center; width: 13.9535%; height: 19px;&quot;&gt;&lt;b&gt;개발 기간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85.9302%; height: 19px;&quot;&gt;2025.05.30 ~ 20225.06.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center; width: 13.9535%; height: 19px;&quot;&gt;&lt;b&gt;협업 팀원&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9; width: 85.9302%; height: 19px;&quot;&gt;JW(main.py + auth.py + navigator.py + problems.py + config.py 담당), resomang (기획 담당)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center; width: 13.9535%; height: 38px;&quot;&gt;&lt;b&gt;개발 목적&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85.9302%; height: 38px;&quot;&gt;PRD Bot에서 더 나아가 담당자의 일을 대신 해주는 프로그램을 만들면 문제를 완전히 방지할 수 있다. 따라서 이를 위한 프로그램을 제작해 업무 자동화를 진행하고자했다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 55px;&quot;&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center; width: 13.9535%; height: 55px;&quot;&gt;&lt;b&gt;개발 목표&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9; width: 85.9302%; height: 55px;&quot;&gt;기획 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://pollen-star-9b0.notion.site/HW2-1-PRD-Creator-202da168857e8007b031e54de8c6595a&quot;&gt;https://pollen-star-9b0.notion.site/HW2-1-PRD-Creator-202da168857e8007b031e54de8c6595a&lt;/a&gt;&lt;br /&gt;기술 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://pollen-star-9b0.notion.site/HW2-1-PRD-Creator-203da168857e8096912cd90313586684&quot;&gt;https://pollen-star-9b0.notion.site/HW2-1-PRD-Creator-203da168857e8096912cd90313586684&lt;/a&gt;&lt;br /&gt;(feat. resomang)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zd3X3/btsO690QVRZ/FYbE0PDLvAKe0GZuqk4FYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zd3X3/btsO690QVRZ/FYbE0PDLvAKe0GZuqk4FYk/img.png&quot; data-alt=&quot;PRD Creater 코드구조도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zd3X3/btsO690QVRZ/FYbE0PDLvAKe0GZuqk4FYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzd3X3%2FbtsO690QVRZ%2FFYbE0PDLvAKe0GZuqk4FYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;475&quot; height=&quot;198&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PRD Creater 코드구조도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(1) main.py&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751796118848&quot; class=&quot;reasonml&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from auth import login_to_boj
from navigator import fill_practice_form, go_to_group_and_create_practice
from problem import get_all_matching_problems, sample_problems, get_recent_problem_ids
from mongo import make_db, save_set
from set_manager import (
    get_next_set_number,
    get_set_composition,
    generate_title,
    calc_practice_period
)

driver = login_to_boj()
group_url = &quot;https://www.acmicpc.net/group/23524&quot;

# 한번에 한 세트만 만들어지게 하려고 반복문에서 제외함
db = make_db()      # DB 연결
recent_problem_ids = get_recent_problem_ids(db)
set_number = get_next_set_number(db)
tiers = get_set_composition(set_number)
# 3세트 자동 생성
for tier in tiers:
    tier_min, tier_max = {
        &quot;Bronze&quot;: (1, 5),
        &quot;Silver&quot;: (6, 10),
        &quot;Gold&quot;: (11, 15),
        &quot;Platinum&quot;: (16, 20)
    }[tier]
    count = 2 if tier == &quot;Platinum&quot; else 8

    all = get_all_matching_problems(tier_min, tier_max, exclude_ids=recent_problem_ids)
    sampled = sample_problems(all, k=count)

    contest_title = generate_title(set_number, tier)
    contest_start, contest_end = calc_practice_period(set_number, tier)

    form_data = {
        &quot;contest_title&quot;: contest_title,
        &quot;contest_start&quot;: contest_start,
        &quot;contest_end&quot;: contest_end,
        &quot;problem_id&quot;: [p[&quot;problemId&quot;] for p in sampled]
    }
    go_to_group_and_create_practice(driver, group_url)
    fill_practice_form(driver, form_data)
    save_set(db,form_data, set_number) # DB에 저장

driver.quit()&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(2) auth.py&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751796118850&quot; class=&quot;pgsql&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;#로그인 자동화

from selenium import webdriver
from selenium.webdriver.common.by import By
from dotenv import load_dotenv
import os
import time

#키 값 가져옴 from .venv
load_dotenv()
USERNAME = os.getenv(&quot;BOJ_ID&quot;)
PASSWORD = os.getenv(&quot;BOJ_PW&quot;)
print(&quot;USERNAME:&quot;, USERNAME)
print(&quot;PASSWORD:&quot;, PASSWORD)

def login_to_boj():
    options = webdriver.ChromeOptions() # 괄호를 붙여주어야 인스턴스가 생성됨
    #크롬 창을 전체 화면 크기로 연다.
    #시크릿 모드로 열고 싶을 때 options.add_argument(&quot;--incogito&quot;)
    #창 없이 백그라운드로 실행하고 싶을 때 options.add_argument(&quot;--headless&quot;)
    options.add_argument(&quot;--start-maximized&quot;)
    driver = webdriver.Chrome(options=options)

    #BOJ 로그인 페이지 접속
    driver.get(&quot;https://www.acmicpc.net/login&quot;)
    time.sleep(1)

    driver.find_element(By.NAME, &quot;login_user_id&quot;).send_keys(USERNAME)
    driver.find_element(By.NAME, &quot;login_password&quot;).send_keys(PASSWORD)
    driver.find_element(By.ID, &quot;submit_button&quot;).click()
    
    #CAPTCHA가 이미지 기반이기 때문에 코드 자체에서 인식하도록 하는 방식은 어렵기 때문에 input과 같은 수동적인 입력 방식으로 순차 처리한다.
    input(&quot;CAPTCHA가 있을 경우 수동으로 해결 후 Enter 키를 눌러주세요...&quot;)

    print(&quot;✅Login Completed✅&quot;)
    return driver&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(3) navigator.py&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751796118852&quot; class=&quot;python&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;#연습 폼 자동 입력

from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time


def go_to_group_and_create_practice(driver, group_url):
    driver.get(group_url)
    time.sleep(2)

    # &quot;연습 만들기&quot; 탭 클릭
    practice_tab = driver.find_element(By.LINK_TEXT, &quot;연습 만들기&quot;)
    practice_tab.click()

    # 연습 폼이 완전히 뜰 때까지 기다림
    WebDriverWait(driver, 3).until(
        EC.presence_of_element_located((By.NAME, &quot;contest_title&quot;))  # 또는 By.ID, &quot;practice_title&quot;
    )

def fill_practice_form(driver, form_data):
    try:
        # 1. 연습 제목 입력
        title_input = driver.find_element(By.NAME, &quot;contest_title&quot;)
        title_input.clear()

        # 이모지 포함된 문자열 safely 입력
        emoji_title = form_data[&quot;contest_title&quot;]
        driver.execute_script(&quot;&quot;&quot;
            arguments[0].value = arguments[1];
            arguments[0].dispatchEvent(new Event('change'));
        &quot;&quot;&quot;, title_input, emoji_title)


        # 2. 시작 시간
        start_input = driver.find_element(By.NAME, &quot;contest_start&quot;)
        start_input.clear()
        start_input.send_keys(form_data[&quot;contest_start&quot;])  # yyyy-mm-dd hh:mm

        # 3. 종료 시간
        end_input = driver.find_element(By.NAME, &quot;contest_end&quot;)
        end_input.clear()
        end_input.send_keys(form_data[&quot;contest_end&quot;])

        # 4. 문제 입력
        for problem_id in form_data[&quot;problem_id&quot;]:
            problem_input = driver.find_element(By.ID, &quot;problem-search&quot;)  # &amp;larr; 실제 ID로 수정
            problem_input.clear()
            problem_input.send_keys(str(problem_id))
            problem_input.send_keys(Keys.ENTER)
            time.sleep(0.2)  # 추가 버튼 누른 효과

        # 5. 프리즈 시간 (30분)
        freezing_input = driver.find_element(By.NAME, &quot;contest_freeze&quot;)
        freezing_input.clear()
        freezing_input.send_keys(&quot;0&quot;)
        freezing_input.send_keys(Keys.TAB)

        # 6. 패널티 (20분)
        penalty_input = driver.find_element(By.NAME, &quot;contest_penalty&quot;)
        penalty_input.clear()
        penalty_input.send_keys(&quot;0&quot;)
        penalty_input.send_keys(Keys.TAB)

        # 패널티 계산: 누적 (기본값 유지)
        # 제출하지 않은 참가자 숨기기
        #    &amp;rarr; 라벨 텍스트 기반으로 클릭 처리 (예: &quot;보여주지 않음&quot; 선택)
        hide_option = driver.find_element(By.XPATH, '//label[contains(text(), &quot;보여주지 않음&quot;)]')
        hide_option.click()


        # 9. &quot;만들기&quot; 버튼 클릭
        driver.find_element(By.ID, &quot;save_button&quot;).click()

        print(&quot;✅ 연습 생성 완료&quot;)

    except NoSuchElementException as e:
        print(&quot;❌ 연습 폼 자동 입력 실패:&quot;, e)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(4) problem.py (problems.py)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751796118855&quot; class=&quot;routeros&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;#문제 수집 및 샘플링

import requests
import random
import time  # 요청 사이에 약간의 텀을 주기 위해 사용

# 기존의 문제 가져오기
def get_recent_problem_ids(db):
    docs = db.test_collection.find({}, {&quot;problem_ids&quot;: 1})
    problem_ids = set()
    for doc in docs:
        problem_ids.update(doc.get(&quot;problem_ids&quot;, []))
    return problem_ids

def get_all_matching_problems(tier_min, tier_max, max_solved=10000, max_pages=20, exclude_ids=None):
    #10000문제 이하, 10000명 이하가 푼 문제 수가 20페이지 이하일 것이다 예상
    #50(한번에 가져올 수 있는 문제 수) x 20(예상 최대 페이지 수) = 1000
    collected = []
    page = 1
    size = 50
    exclude_ids = exclude_ids or set() # 기존 문제를 집합으로 가져옴

    while True:
        query = f&quot;tier:{tier_min}..{tier_max} solved:&amp;lt;{max_solved}&quot;
        url = &quot;https://solved.ac/api/v3/search/problem&quot;
        #search/problem 문제 풀이 관련 API 호출

        params = {
            &quot;query&quot;: query,
            &quot;page&quot;: page,
            &quot;size&quot;: size
        }

        response = requests.get(url, params=params)
        #정수 200으로 확인하는 이유는 서버가 상태 코드를 포함한 응답
        #으로 보내는데 요청이 성공적으로 처리되었을 때 반환되는 값이 200이다.
        if response.status_code != 200:
            print(&quot;API 호출 실패:&quot;, response.status_code)
            break

        #데이터를 json 형식으로 파싱
        data = response.json()
        
        #items 키는 실제 문제들의 리스트를 가지고 있음
        items = data.get(&quot;items&quot;, [])

        # 중복 문제 필터링
        new_items = [item for item in items if item[&quot;problemId&quot;] not in exclude_ids]
        
        #문제들을 리스트에 저장
        collected.extend(new_items)

        # 마지막 페이지까지 왔다면 중단
        if page * size &amp;gt;= data[&quot;count&quot;]:
            break

        page += 1
        if page &amp;gt; max_pages:  # API 부하 방지를 위한 안전장치
            break
        time.sleep(0.2)  # 속도 제한 방지용 딜레이

    return collected

def sample_problems(problems, k=8):
    return random.sample(problems, k=min(k, len(problems)))&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(5) mongo.py (db.py)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751796118859&quot; class=&quot;routeros&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# aha 파트
# ※ Pymongo 
# - MongoDB를 Python으로 사용하기 위해 사용하는 라이브러리
import pymongo
from set_manager import get_next_set_number

# ※ main.py 실행 시, DB생성
# - MongoClient() : (옵션 포함)인스턴스 생성 -&amp;gt; MongoDB와 연결
# - get_database() : 사용할 DB를 지정 &amp;amp; 연결
def make_db():
    client = pymongo.MongoClient(&quot;mongodb://ynhea:0000@localhost:27017/&quot;)
    db_name = &quot;testdb&quot;
    db = client.get_database(db_name)
    return db

# ※ Collection 생성
def save_set(db, form_data, set_number):
    db.test_collection.insert_one({
        &quot;set_number&quot;:   set_number,
        &quot;title&quot;:        form_data[&quot;contest_title&quot;],
        &quot;problem_ids&quot;:  form_data[&quot;problem_id&quot;],
        &quot;start_date&quot;:   form_data[&quot;contest_start&quot;],
        &quot;end_date&quot;:     form_data[&quot;contest_end&quot;]
    })&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(6) .env&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(config.py) (보안상의 이유로 .env파일로 분리한 후, os.getenv()를 활용해 가져오도록 한다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1135&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsd2lz/btsO7RemnRu/cLizHtzjrDUBasIJGCKuKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsd2lz/btsO7RemnRu/cLizHtzjrDUBasIJGCKuKk/img.png&quot; data-alt=&quot;PRD Creater 실행결과 (백준)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsd2lz/btsO7RemnRu/cLizHtzjrDUBasIJGCKuKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdsd2lz%2FbtsO7RemnRu%2FcLizHtzjrDUBasIJGCKuKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1135&quot; height=&quot;533&quot; data-origin-width=&quot;1135&quot; data-origin-height=&quot;533&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PRD Creater 실행결과 (백준)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cb3mG/btsO6iRJ1v6/R4BG6Pof6hTTiIPiN8Jcg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cb3mG/btsO6iRJ1v6/R4BG6Pof6hTTiIPiN8Jcg1/img.png&quot; width=&quot;497&quot; height=&quot;320&quot; data-phocus-index=&quot;4&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;320&quot; data-is-animation=&quot;false&quot; style=&quot;width: 33.4511%; margin-right: 10px;&quot; data-widthpercent=&quot;34.25&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cb3mG/btsO6iRJ1v6/R4BG6Pof6hTTiIPiN8Jcg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCb3mG%2FbtsO6iRJ1v6%2FR4BG6Pof6hTTiIPiN8Jcg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;497&quot; height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EXix0/btsO5AllkUY/pMKIoP8HNNN0lq5x36ZhXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EXix0/btsO5AllkUY/pMKIoP8HNNN0lq5x36ZhXK/img.png&quot; width=&quot;472&quot; height=&quot;312&quot; data-phocus-index=&quot;5&quot; data-origin-width=&quot;472&quot; data-origin-height=&quot;312&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.583%; margin-right: 10px;&quot; data-widthpercent=&quot;33.36&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EXix0/btsO5AllkUY/pMKIoP8HNNN0lq5x36ZhXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEXix0%2FbtsO5AllkUY%2FpMKIoP8HNNN0lq5x36ZhXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;472&quot; height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZiTnu/btsO6YZAUQj/7TP2nw2TkKUXZWyLskERr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZiTnu/btsO6YZAUQj/7TP2nw2TkKUXZWyLskERr1/img.png&quot; width=&quot;451&quot; height=&quot;307&quot; data-phocus-index=&quot;6&quot; data-origin-width=&quot;451&quot; data-origin-height=&quot;307&quot; data-is-animation=&quot;false&quot; style=&quot;width: 31.6404%;&quot; data-widthpercent=&quot;32.39&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZiTnu/btsO6YZAUQj/7TP2nw2TkKUXZWyLskERr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZiTnu%2FbtsO6YZAUQj%2F7TP2nw2TkKUXZWyLskERr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;451&quot; height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;PRD Creater 실행결과 (MongoDB)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: center;&quot;&gt;aha 파트&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff; text-align: center;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center;&quot;&gt;main.py 일부&lt;/td&gt;
&lt;td&gt;- make_db()를 활용해 mongoDB 생성&lt;br /&gt;- problem.py에서 get_recent_problem_ids()를 활용해 이전 문제들을 가져옴&lt;br /&gt;- get_all_matching_problems()를 활용해 문제 생성&lt;br /&gt;- 만들어진 form_data를 set_db()를 활용해 DB에 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center;&quot;&gt;problem.py 일부&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;- DB에서 기존 문제들의 번호만을 추출해 가져오는 get_recent_problem_ids() 제작&lt;br /&gt;- 중복 문제를 필터링한 후, 중복되지 않는 최종 문제들을 리스트에 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef; text-align: center;&quot;&gt;mongo.py&lt;/td&gt;
&lt;td&gt;- MongoClient()를 활용해 DB와 연결&lt;br /&gt;- insert_one()을 활용해 Collection을 생성한 후, 매개변수로 받아온 form_data를 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 느낀점&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(1) 문서화의 중요성&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- resomang님이 프로젝트 전에 기획서와 기획(기술)문서를 제공해주셨다. 이전까지는 만들고자 하는 프로그램의 목적과 코드구조도만 대략적으로 구상한 뒤, 코드를 직접 적으며 세부적인 것들을 만들어 갔었다. 하지만 이번 프로젝트를 계기로 하여 기획문서작성이 얼마나 프로젝트를 진행하는 데에 있어서 중요한 지를 몸소 느낄 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 특히, &amp;lt;기획(기술)문서의 2. 주요 기술 스택 및 도구&amp;gt; 파트는 처음에 읽고 정말 환상적이었다. (뭔가 부족했던 과거 문서화의 아쉬움이 탁 트이는 기분이었다.) 물론 처음에 기술문서를 접했을 땐 뭔소린지 이해가 하나도 안 되었지만, 개발과정에서 이걸 만드려면 뭐가 필요한지 정해져있기 때문에, 쓸데없는 데 방황하는 삽질시간을 대폭 절감할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 이뿐만 아니라, 기획문서와 시스템 구성도 등을 통해 시스템이 돌아가는 것을 문서로 체계화하는 방법을 배울 수 있었다. 따라서 개인적으로 개발역량을 강화시키는 데에 있어서 매우 좋은 경험이었고, 이 자리를 빌어 resomang님에게 존경을 표하고 싶다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(2) 협업과정에서의 모듈화 &amp;amp; 팀워크의 중요성&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 이번 프로젝트의 90%를 JW님이 맡아주셨다. 처음에는 90%를 이해하지 못 하면 어떻게 하지라는 막연한 두려움이 있었던 게 사실이다. 하지만, 기능에 따른 주석처리와 모듈화, DB와 관련된 부분을 주석으로 처리하는 등 팀원을 위한 노력이 곳곳에 분포되어 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 특히, 주석처리는 코드작성에 비해 상대적으로 덜 중요한 요소지만 시간이 꽤 걸리는 작업이기 때문에, 인공지능을 이용해 간단히 처리하는 것을 알 수 있었다. 오늘날 개발자가 인공지능을 제대로 사용하는 게 중요하다고 했던 교수님의 말씀이 바로 이걸 의미하는 것임을 깨달을 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 늦은 시간임에도 빠른 피드백을 진행해주고, 부족한 나를 포기하지 않았던 JW님에게도 이 자리를 빌어 감사인사를 전하고 싶다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&amp;nbsp;4. 아쉬운 점&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn5hr4/btsO60QA0nc/7gdBnyUtpK9Yv3IKIKtlSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn5hr4/btsO60QA0nc/7gdBnyUtpK9Yv3IKIKtlSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn5hr4/btsO60QA0nc/7gdBnyUtpK9Yv3IKIKtlSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn5hr4%2FbtsO60QA0nc%2F7gdBnyUtpK9Yv3IKIKtlSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;295&quot; height=&quot;192&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 이번 프로젝트에서 만들고자 했던 기술 스택 중, 스케쥴링 부분은 시간이 빠듯해 완성하지 못 했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- PRD Bot 프로젝트를 진행하면서 컴퓨터가 꺼지거나, 이전에 실행하지 않을 경우엔 연습세션이 제작되지 않는다는 치명적 단점을 발견했었다. 이 점을 개선시킬 수 있는 파트가 바로 스케쥴링이었기 때문에, 약간의 아쉬움이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 하지만 앞으로 배울 웹기술들과 연동시켜 프로젝트를 점차 확장해나갈 계획이기 때문에, 약간의 아쉬움을 앞으로의 발전을 위한 계기로 삼고자 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 확장&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 게임과 모바일, 임베디드보다 웹에 관심을 가지고 있고, 같이 협업을 진행한 JW님도 백엔드에 흥미를 보이고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 따라서, 앞으로는 다양한 웹기술을 학습해 PRD Creater프로젝트를 확장시켜 배포까지 해볼 듯 하다.&lt;/p&gt;</description>
      <category>Project/Expotential PS</category>
      <category>AHA</category>
      <category>discord</category>
      <category>javasript</category>
      <category>jw</category>
      <category>MongoDB</category>
      <category>prd bot</category>
      <category>prd creater</category>
      <category>python</category>
      <category>resomang</category>
      <category>Selenium</category>
      <author>aha0</author>
      <guid isPermaLink="true">https://aha-log.tistory.com/1</guid>
      <comments>https://aha-log.tistory.com/1#entry1comment</comments>
      <pubDate>Sun, 6 Jul 2025 19:04:11 +0900</pubDate>
    </item>
  </channel>
</rss>