Code N Solve
๐: ์๋ผ๋ ์๋ ๋ก๊ทธ์ธ๊ณผ ๋์ ์์ง ์๋ํ ์คํจ ์ฌ๋ก ์ ๋ฆฌ
์ต๊ทผ์ ๊ณผ๊ฑฐ ๊ตฌ๋งคํ ๋์ ๋ด์ญ์ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ ์ถ์ด์, ์๋ผ๋์์ ์ฃผ๋ฌธ ๋ด์ญ์ ์๋์ผ๋ก ์์งํ๋ ์์คํ ์ ๊ตฌ์ถํด๋ณด์๋ค.
Playwright1๋ฅผ ์ฌ์ฉํ ๋ธ๋ผ์ฐ์ ์๋ํ์ GitHub Actions2๋ฅผ ํตํ ์ฃผ๊ธฐ์ ์คํ์ผ๋ก ์์ ์๋ํ๋ฅผ ๋ชฉํ๋ก ํ์ง๋ง, ์์๋ณด๋ค ํจ์ฌ ๋ง์ ๋ฌธ์ ์ ์ง๋ฉดํ๋ค.
ํนํ headless Chrome ํ๊ฒฝ์์์ ๋ก๊ทธ์ธ ์ฐจ๋จ, ์ธ์ ๊ด๋ฆฌ ๋ณต์ก์ฑ, Git ์๋ ์ปค๋ฐ ์ค๋ฅ ๋ฑ ๋ค์ํ ์คํจ ์ฌ๋ก์ ํด๊ฒฐ ๊ณผ์ ์ ์ ๋ฆฌํด๋ณด์๋ค.
๐จ 1. Playwright ์๋ ๋ก๊ทธ์ธ ์คํจ - ๋ด ํ์ง ์ฐจ๋จ
๐จ ๋ฌธ์ ์ํฉ
โ ๋ก๊ทธ์ธ ์คํจ: waiting for navigation to "**/account/wmaininfo.aspx*" until 'load'
navigated to "https://www.aladin.co.kr/login/wlogin_popup_result.aspx?SecureOpener=1"
๐ง ์์ธ ๋ถ์
-
์๋ผ๋์ ๋ก๊ทธ์ธ ํ์ด์ง(
wlogin_popup.aspx?SecureOpener=1
)๋ ํ์ ์ ์ฉ ํ์ด์ง๋ก ์ค๊ณ๋์ด ์์ผ๋ฉฐ, ์๋ํ ๋ธ๋ผ์ฐ์ ํ์ง๊ฐ ๋งค์ฐ ๊ฐ๋ ฅํ๊ฒ ์ ์ฉ๋จ. -
GitHub Actions์ headless Chromium ํ๊ฒฝ์์๋ ๋ค์๊ณผ ๊ฐ์ ํน์ง๋ค์ด ์๋ํ ๋๊ตฌ๋ก ์ธ์๋จ:
navigator.webdriver
์์ฑ์ดtrue
๋ก ์ค์ - ์ผ๋ฐ ์ฌ์ฉ์์ ๋ค๋ฅธ
User-Agent
ํค๋ - JavaScript ์คํ ํจํด์ ์ฐจ์ด
- ๋ง์ฐ์ค/ํค๋ณด๋ ์ด๋ฒคํธ ํ์ด๋ฐ์ ๋ถ์์ฐ์ค๋ฌ์
-
ํนํ
SecureOpener=1
ํ๋ผ๋ฏธํฐ๊ฐ ํฌํจ๋ ํ์ ํ์ด์ง๋window.opener
๊ฐ์ฒด๋ฅผ ์๊ตฌํ๊ฑฐ๋ ํน์ JavaScript ์ปจํ ์คํธ์์๋ง ์ ์ ์๋ํ๋๋ก ์ ํ๋์ด ์์.
โ ํด๊ฒฐ ๋ฐฉ๋ฒ
1. ๋ธ๋ผ์ฐ์ ํ์ง ์ฐํ ์ค์
# Playwright ๋ธ๋ผ์ฐ์ ์ค์
browser = await playwright.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled",
"--disable-web-security",
"--disable-features=VizDisplayCompositor"
]
)
# JavaScript ์์ฑ ์กฐ์์ผ๋ก ํ์ง ์ฐํ
context.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3]});
Object.defineProperty(navigator, 'languages', {get: () => ['ko-KR', 'ko']});
window.chrome = { runtime: {} };
""")
2. ์์ฐ์ค๋ฌ์ด ์ ๋ ฅ ์๋ฎฌ๋ ์ด์
# ์ผ๋ฐ ์ฌ์ฉ์์ ์ ์ฌํ ์
๋ ฅ ํจํด ์ ์ฉ
await page.type("#Email", ALADIN_ID, delay=100) # 100ms ๊ฐ๊ฒฉ์ผ๋ก ํ์ดํ
await page.type("#Password", ALADIN_PW, delay=150)
await page.wait_for_timeout(1000) # ์
๋ ฅ ํ 1์ด ๋๊ธฐ
# ๋ค์ํ ๋ก๊ทธ์ธ ์๋ ๋ฐฉ์ ์ ์ฉ
try:
await page.click("input[type='submit'][value='๋ก๊ทธ์ธ']")
except:
await page.eval_on_selector("#LoginForm", "form => form.submit()")
3. ๋ก๊ทธ์ธ ์ฑ๊ณต ํ๋จ ๋ก์ง ๊ฐ์
# URL ๋ณํ ๋์ DOM ์์๋ก ์ฑ๊ณต ์ฌ๋ถ ํ๋จ
await page.wait_for_timeout(3000)
if page.query_selector("#Email") and await page.is_visible("#Email"):
print("โ ๋ก๊ทธ์ธ ์คํจ: ๋ก๊ทธ์ธ ํผ์ด ์ฌ์ ํ ํ์๋จ")
return False
else:
print("โ
๋ก๊ทธ์ธ ์ฑ๊ณต์ผ๋ก ์ถ์ ๋จ")
return True
๐จ 2. ์ธ์ ํ์ผ ์์ - GitHub Actions ํ๊ฒฝ ๋ฌธ์
โ ๋ฌธ์ ์ํฉ
FileNotFoundError: [Errno 2] No such file or directory: 'storage/aladin_storage.json'
๐ง ์์ธ ๋ถ์
- GitHub Actions์์
save_aladin_session.py
๊ฐ ๋ก๊ทธ์ธ ์คํจ๋ก ์ธํด ์ธ์ ํ์ผ(aladin_storage.json
) ์์ฑ์ ์คํจ.- GitHub Actions ํ๊ฒฝ์์ Playwright๊ฐ ๋ก๊ทธ์ธ์ ์คํจํ๊ธฐ ๋๋ฌธ์ด๋ฉฐ, ์ด๋ ์ธ์
์ ์ฅ ์ด์ ๋จ๊ณ์์ ์์ ํ ์ค๋จ๋๊ธฐ ๋๋ฌธ์ ์ดํ
fetch
์คํฌ๋ฆฝํธ๊ฐ ๋ฌด์๋ฏธํด์ง.
- GitHub Actions ํ๊ฒฝ์์ Playwright๊ฐ ๋ก๊ทธ์ธ์ ์คํจํ๊ธฐ ๋๋ฌธ์ด๋ฉฐ, ์ด๋ ์ธ์
์ ์ฅ ์ด์ ๋จ๊ณ์์ ์์ ํ ์ค๋จ๋๊ธฐ ๋๋ฌธ์ ์ดํ
- ์ดํ
fetch_aladin.py
์คํ ์ ์กด์ฌํ์ง ์๋ ์ธ์ ํ์ผ์ ์ฐธ์กฐํ๋ ค๊ณ ํด์FileNotFoundError
๋ฐ์. - Playwright์
storage_state
์ต์ ์ ๋ฐ๋์ ์ ํจํ ํ์ผ ๊ฒฝ๋ก๋ฅผ ์๊ตฌํ๋ฏ๋ก, ํ์ผ์ด ์กด์ฌํ์ง ์์ผ๋ฉด ๋ธ๋ผ์ฐ์ ์ปจํ ์คํธ ์์ฑ ์์ฒด๊ฐ ์คํจํจ.
โ ํด๊ฒฐ ๋ฐฉ๋ฒ
1. ์ธ์ ์ ํจ์ฑ ๊ฒ์ฌ ํจ์ ๊ตฌํ
def verify_session():
"""์ ์ฅ๋ ์ธ์
์ด ์ ํจํ์ง ํ์ธ"""
if not os.path.exists("storage/aladin_storage.json"):
print("โ ์ธ์
ํ์ผ์ด ์กด์ฌํ์ง ์์ต๋๋ค")
return False
try:
with open("storage/aladin_storage.json", "r") as f:
session_data = json.load(f)
# ์ธ์
๋ฐ์ดํฐ ๊ตฌ์กฐ ํ์ธ
if not session_data.get("cookies") or not session_data.get("origins"):
print("โ ์ธ์
๋ฐ์ดํฐ๊ฐ ์ ํจํ์ง ์์ต๋๋ค")
return False
print("โ
๊ธฐ์กด ์ธ์
์ด ์ ํจํฉ๋๋ค")
return True
except Exception as e:
print(f"โ ์ธ์
ํ์ธ ์ค ์ค๋ฅ: {e}")
return False
# fetch_aladin.py ๋ด๋ถ
if not STORAGE_FILE.exists():
print("โ ์ธ์
ํ์ผ์ด ์์ต๋๋ค. ๋จผ์ ๋ก๊ทธ์ธ์ ์คํํ์ธ์.")
return
- fetch ์คํฌ๋ฆฝํธ๊ฐ ์ธ์ ํ์ผ์ด ์์ผ๋ฉด ์ฆ์ ์ข ๋ฃ๋๋๋ก ์์ ์ฅ์น ์ถ๊ฐ.
2. GitHub Actions ์บ์ ์ ๋ต
- name: Restore Aladin session from cache
id: restore-session
uses: actions/cache@v4
with:
path: storage/aladin_storage.json
key: aladin-session-v2-${{ secrets.ALADIN_ID }}-${{ github.run_number }}
restore-keys: |
aladin-session-v2-${{ secrets.ALADIN_ID }}-
- name: Generate or verify Aladin session
env:
ALADIN_ID: ${{ secrets.ALADIN_ID }}
ALADIN_PW: ${{ secrets.ALADIN_PW }}
run: |
echo "๐ ์ธ์
์ํ ํ์ธ ๋ฐ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ..."
python scripts/save_aladin_session.py
- name: Save session to cache
if: success()
uses: actions/cache/save@v4
with:
path: storage/aladin_storage.json
key: aladin-session-v2-${{ secrets.ALADIN_ID }}-${{ github.run_number }}
3. ์กฐ๊ฑด๋ถ ์ธ์ ์ฒ๋ฆฌ ๋ก์ง
# save_aladin_session.py
if verify_session():
print("โ
๊ธฐ์กด ์ธ์
์ด ์ ํจํฉ๋๋ค")
sys.exit(0)
else:
print("๐ ์ ์ธ์
์ ์์ฑํฉ๋๋ค")
if save_aladin_session():
print("โ
์ธ์
์ ์ฅ ์๋ฃ")
else:
print("โ ์ธ์
์์ฑ ์คํจ")
sys.exit(1)
๐จ 3. Git ๋ธ๋์น ์ถฉ๋ - origin/main vs origin/master
โ ๋ฌธ์ ์ํฉ
fatal: invalid upstream 'origin/main'
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.
Error: Process completed with exit code 1.
๐ง ์์ธ ๋ถ์
- ์ ์ฅ์์ ๊ธฐ๋ณธ ๋ธ๋์น๊ฐ
master
์ธ๋ฐ๋ ์ํฌํ๋ก์ฐ์์origin/main
์ผ๋ก ๋ฆฌ๋ฒ ์ด์ค๋ฅผ ์๋ํจ. git rebase
์คํ ์ ์ ๋ณ๊ฒฝ์ฌํญ์ด ์ ๋๋ก ์คํ ์ด์ง๋์ง ์์ "unstaged changes" ์ค๋ฅ ๋ฐ์.- Git ๋ช ๋ น์ด ์คํ ์์์ ๋ธ๋์น ๊ฐ์ง ๋ก์ง์ ๋ฌธ์ ๊ฐ ์์์.
โ ํด๊ฒฐ ๋ฐฉ๋ฒ
1. ๋ธ๋์น ์๋ ๊ฐ์ง ๋ก์ง
- name: Detect default branch
id: branch
run: |
if git ls-remote --heads origin main | grep -q main; then
echo "default_branch=main" >> $GITHUB_OUTPUT
else
echo "default_branch=master" >> $GITHUB_OUTPUT
fi
echo "ํ์ฌ ๋ธ๋์น: $(git branch --show-current)"
echo "๊ธฐ๋ณธ ๋ธ๋์น: $(cat $GITHUB_OUTPUT | grep default_branch)"
2. ์์ ํ ์ปค๋ฐ ๋ฐ ํธ์ ๋ก์ง
- name: Commit and push changes
if: steps.changes.outputs.changed == 'true'
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
# ๋ณ๊ฒฝ์ฌํญ ํ์ธ ๋ฐ ์คํ
์ด์ง
if ! git diff --quiet data/; then
echo "๐ ๋ณ๊ฒฝ์ฌํญ์ด ํ์ธ๋จ, ์ปค๋ฐ ์งํ..."
git status
git add data/*.json
# ์คํ
์ด์ง๋ ๋ณ๊ฒฝ์ฌํญ์ด ์์ ๋๋ง ์ปค๋ฐ
if ! git diff --cached --quiet; then
git commit -m "chore: update aladin book list $(date '+%Y-%m-%d %H:%M')"
# ์์ ํ ํธ์ ์ฌ์๋ ๋ก์ง
for i in {1..5}; do
echo "๐ ํธ์ ์๋ ${i}/5..."
git fetch origin ${{ steps.branch.outputs.default_branch }}
if git rebase origin/${{ steps.branch.outputs.default_branch }}; then
if git push origin HEAD:${{ steps.branch.outputs.default_branch }}; then
echo "โ
ํธ์ ์ฑ๊ณต!"
break
fi
fi
if [ $i -lt 5 ]; then
echo "โณ 10์ด ๋๊ธฐ ํ ์ฌ์๋..."
sleep 10
fi
done
fi
fi
๐จ 4. ๋์ ์์ง ์คํฌ๋ฆฝํธ ์คํจ - DOM ๊ตฌ์กฐ ๋ณํ ๋์
โ ๋ฌธ์ ์ํฉ
โ ์ฃผ๋ฌธ ๋ด์ญ์ ์ฐพ์ ์ ์์ต๋๋ค
โ ๋์ ์ ๋ณด ์ถ์ถ ์คํจ
๐ง ์์ธ ๋ถ์
- ์๋ผ๋ ์น์ฌ์ดํธ์ DOM ๊ตฌ์กฐ๊ฐ ์์๋ก ๋ณ๊ฒฝ๋์ด ๊ณ ์ ๋ CSS ์ ๋ ํฐ๋ก๋ ์์๋ฅผ ์ฐพ์ ์ ์์.
- ํ์ด์ง ๋ก๋ฉ ํ์ด๋ฐ์ ๋ฐ๋ผ JavaScript๋ก ๋์ ์์ฑ๋๋ ์์๋ค์ด ๋ฆ๊ฒ ๋ก๋๋จ.
- ๋ก๊ทธ์ธ ์ํ๊ฐ ์ ์ง๋์ง ์์ ์ฃผ๋ฌธ ๋ด์ญ ํ์ด์ง์ ์ ๊ทผํ ์ ์๋ ๊ฒฝ์ฐ ๋ฐ์.
โ ํด๊ฒฐ ๋ฐฉ๋ฒ
1. ๋ค์ค ์ ๋ ํฐ ๋ฐ Fallback ์ ๋ต
def extract_books_from_page(page):
"""์ฌ๋ฌ ์
๋ ํฐ๋ฅผ ์๋ํ์ฌ ์ฃผ๋ฌธ ๋ด์ญ ์ถ์ถ"""
selectors = [
"tbody#tblOrdersItem > tr", # ๊ธฐ๋ณธ ์
๋ ํฐ
".order-info tr", # ๋์ฒด ์
๋ ํฐ 1
"table tr", # ๋์ฒด ์
๋ ํฐ 2
".order-list-item" # ๋์ฒด ์
๋ ํฐ 3
]
for selector in selectors:
elements = page.query_selector_all(selector)
if elements:
print(f"โ
์์ ๋ฐ๊ฒฌ: {selector} ({len(elements)}๊ฐ)")
return elements
print("โ ๋ชจ๋ ์
๋ ํฐ๋ก ์์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
return []
2. ๋ก๊ทธ์ธ ์ํ ํ์ธ ํจ์
def check_login_status(page):
"""ํ์ฌ ํ์ด์ง์์ ๋ก๊ทธ์ธ ์ํ ํ์ธ"""
# ๋ก๊ทธ์ธ์ด ํ์ํ ํ์ด์ง์ ํน์ง์ ์์ ํ์ธ
login_indicators = [
"#Email", # ๋ก๊ทธ์ธ ํผ
".login-form", # ๋ก๊ทธ์ธ ํด๋์ค
"input[name='Email']" # ์ด๋ฉ์ผ ์
๋ ฅ
]
for indicator in login_indicators:
if page.query_selector(indicator):
return False # ๋ก๊ทธ์ธ ํผ์ด ๋ณด์ด๋ฉด ๋ฏธ๋ก๊ทธ์ธ ์ํ
return True # ๋ก๊ทธ์ธ ํผ์ด ์์ผ๋ฉด ๋ก๊ทธ์ธ ์ํ๋ก ์ถ์
3. ๋๋ฒ๊ทธ ์ ๋ณด ์์ง
def safe_goto(page, url, timeout=30000):
"""์์ ํ ํ์ด์ง ์ด๋ ๋ฐ ๋๋ฒ๊ทธ ์ ๋ณด ์์ง"""
try:
page.goto(url, wait_until="networkidle", timeout=timeout)
print(f"โ
ํ์ด์ง ๋ก๋ ์ฑ๊ณต: {url}")
return True
except Exception as e:
print(f"โ ํ์ด์ง ๋ก๋ ์คํจ: {url}")
print(f"์ค๋ฅ: {e}")
# ๋๋ฒ๊ทธ์ฉ ์คํฌ๋ฆฐ์ท ์ ์ฅ
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
screenshot_path = f"debug_error_{timestamp}.png"
page.screenshot(path=screenshot_path)
print(f"๐ธ ์คํฌ๋ฆฐ์ท ์ ์ฅ: {screenshot_path}")
return False
- ์คํจ ์ ๋๋ฒ๊น ์ด๋ฏธ๋ ์ ์ฅ ๋ฐ ์ ๋ก๋ํ์ฌ ํ์ธ ๊ฐ๋ฅํ๋๋ก ๊ตฌ์ฑ.
๐จ 5. GitHub Actions ์คํ ์๊ฐ ์ด๊ณผ
โ ๋ฌธ์ ์ํฉ
Error: The operation was canceled.
๐ง ์์ธ ๋ถ์
- Playwright์ ๋ธ๋ผ์ฐ์ ์คํ๊ณผ ํ์ด์ง ๋ก๋ฉ์ด ์์๋ณด๋ค ์ค๋ ๊ฑธ๋ฆผ.
- ํนํ headless ํ๊ฒฝ์์ JavaScript ์คํ์ด ๋๋ ค์ง๋ ๊ฒฝ์ฐ๊ฐ ์์.
- ๋คํธ์ํฌ ์ง์ฐ์ด๋ ์๋ผ๋ ์๋ฒ ์๋ต ์ง์ฐ ์ timeout ๋ฐ์.
โ ํด๊ฒฐ ๋ฐฉ๋ฒ
1. ์ ์ ํ timeout ์ค์ ๋ฐ ์ฌ์๋ ๋ก์ง
jobs:
fetch-books:
runs-on: ubuntu-latest
timeout-minutes: 30 # ์ถฉ๋ถํ ์คํ ์๊ฐ ํ๋ณด
steps:
- name: Run fetch script with retry
run: |
echo "๐ ์ฑ
์ ๋ณด ์์ง ์์..."
python scripts/fetch_aladin.py || {
echo "์ฒซ ๋ฒ์งธ ์๋ ์คํจ, 30์ด ํ ์ฌ์๋..."
sleep 30
python scripts/fetch_aladin.py
}
2. Playwright timeout ์ต์ ํ
# ๋ธ๋ผ์ฐ์ ๋ฐ ํ์ด์ง timeout ์ค์
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context(
storage_state="storage/aladin_storage.json" if os.path.exists("storage/aladin_storage.json") else None
)
# ํ์ด์ง๋ณ timeout ์ค์
page = await context.new_page()
page.set_default_timeout(60000) # 60์ด
page.set_default_navigation_timeout(60000) # 60์ด
๊ฒฐ๋ก
- ๋ธ๋ผ์ฐ์ ์๋ํ ํ์ง ์ฐํ:
navigator.webdriver
์ ๊ฑฐ, ์์ฐ์ค๋ฌ์ด ์ ๋ ฅ ํจํด, User-Agent ์กฐ์์ผ๋ก ๋ด ํ์ง ํํผ - ์ธ์ ๊ด๋ฆฌ ์ฒด๊ณํ: GitHub Actions ์บ์๋ฅผ ํ์ฉํ ์ธ์ ์ ์ฅ/๋ณต์ ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง ๊ตฌํ
- Git ์๋ํ ์์ ํ: ๋ธ๋์น ์๋ ๊ฐ์ง, ๋ณ๊ฒฝ์ฌํญ ํ์ธ ํ ์ปค๋ฐ, ํธ์ ์ฌ์๋ ๋ก์ง์ผ๋ก ์์ ์ฑ ํ๋ณด
- DOM ๋ณํ ๋์: ๋ค์ค ์ ๋ ํฐ, Fallback ์ ๋ต, ๋ก๊ทธ์ธ ์ํ ํ์ธ์ผ๋ก ์น์ฌ์ดํธ ๋ณํ์ ๋์
- ๋๋ฒ๊ทธ ์ ๋ณด ์์ง: ์คํจ ์ ์คํฌ๋ฆฐ์ท ์ ์ฅ, ์์ธ ๋ก๊ทธ ์ถ๋ ฅ์ผ๋ก ๋ฌธ์ ์ง๋จ ์ฉ์ด์ฑ ํ๋ณด
- ์ ์ ํ timeout ์ค์ : GitHub Actions์ Playwright ๋ชจ๋์์ ์ถฉ๋ถํ ์คํ ์๊ฐ ํ๋ณด
์๋ผ๋๊ณผ ๊ฐ์ ๋ํ ์ผํ๋ชฐ ์ฌ์ดํธ์ ์๋ํ๋ ์์๋ณด๋ค ๊น๋ค๋ก์ ์ง๋ง, ์ฒด๊ณ์ ์ธ ์ ๊ทผ๊ณผ ์ถฉ๋ถํ ์์ธ ์ฒ๋ฆฌ๋ฅผ ํตํด ์์ ์ ์ธ ์๋ํ ์์คํ ์ ๊ตฌ์ถํ ์ ์์๋ค. ํนํ ์คํจ์ ๋ํ ๋ค์ํ Fallback ์ ๋ต๊ณผ ๋๋ฒ๊ทธ ์ ๋ณด ์์ง์ด ํต์ฌ์ด์์ผ๋ฉฐ, ์ด๋ ๋ค๋ฅธ ์น์ฌ์ดํธ ์๋ํ ํ๋ก์ ํธ์๋ ์ ์ฉํ ์ ์๋ ์ค์ํ ๊ตํ์ด๋ค!