brunch

"그랜저·싼타페도 이렇게..." 신형 전기차의 파장

by 리포테라

테슬라 느낌, 미래를 담다
현대차, 차세대 전기차 실내 전격 유출
첫 ‘플레오스 커넥트’ 탑재 모델

Inside-Ioniq-3-1024x576.jpg

콘셉트 쓰리 / 출처 = 현대자동차 (이해를 돕기 위한 사진)


아이오닉3의 스파이샷이 공개되며 테슬라를 연상시키는 대형 디스플레이와 새로운 인포테인먼트 시스템이 주목받고 있다.



2026년 출시 예정인 아이오닉3는 단순한 신차를 넘어, 현대차 내부의 전략적 전환점을 예고하는 시금석이 될 전망이다. 특히 차세대 플랫폼 ‘플레오스 커넥트’를 처음 탑재하며 향후 그랜저·싼타페 등 주요 라인업에 확산될 가능성이 제기된다.


‘플로팅 스크린’과 미래형 레이아웃


%EC%BD%98%EC%85%89%ED%8A%B8-%EC%93%B0%EB%A6%AC-1-1024x683.jpg

콘셉트 쓰리 / 출처 = 현대자동차 (이해를 돕기 위한 사진)


전기차 전문매체 일렉트렉이 4일(현지시각) 보도한 내용에 따르면, 현대차 아이오닉3의 실내 이미지가 처음으로 온라인에 공개됐다. 유출된 사진 속에는 가로형 대형 디스플레이가 중앙에 배치돼 테슬라 모델3/Y와 유사한 구성을 보였다.



스티어링 휠은 타원형으로 변경됐고, 중앙에는 현대차 로고 대신 픽셀 라이트를 상징하는 ‘4개의 점’이 자리 잡았다. 계기판 클러스터는 얇은 스트립 형태로 바뀌며 헤드업 디스플레이의 역할까지 겸할 것으로 보인다.



디지털화가 가속화되는 가운데서도 공조기 조작을 위한 물리 버튼은 유지됐다. 사용자의 직관적인 조작을 고려한 설계다.


플레오스 커넥트, 차량의 두뇌가 되다


%EC%BD%98%EC%85%89%ED%8A%B8-%EC%93%B0%EB%A6%AC-2-1024x683.jpg

콘셉트 쓰리 / 출처 = 현대자동차 (이해를 돕기 위한 사진)


이번에 포착된 아이오닉3 실내의 핵심은 단연 ‘플레오스 커넥트’다. 현대차가 자체 개발한 이 플랫폼은 기존 인포테인먼트 시스템의 업그레이드를 넘어, 차량의 OS와 핵심 기능까지 통합하는 차세대 소프트웨어다.



업계 관계자에 따르면, NVIDIA Drive 기반의 인공지능 연산 기능을 탑재했으며, 차량의 모든 소프트웨어를 무선으로 업데이트하는 ‘Full OTA’를 지원한다. 이로 인해 아이오닉3의 실내는 기존 현대차 모델과 전혀 다른 분위기로 재편됐다.


2026년 유럽 생산, 소형 전기차 전략 본격화


%EC%BD%98%EC%85%89%ED%8A%B8-%EC%93%B0%EB%A6%AC-1024x683.jpg

콘셉트 쓰리 / 출처 = 현대자동차 (이해를 돕기 위한 사진)


아이오닉3는 지난 뮌헨 모터쇼에서 공개된 ‘콘셉트 쓰리’를 기반으로 양산된다. 차체 크기는 길이 4287mm, 폭 1940mm, 높이 1428mm로, 폭스바겐 ID.3나 기아 EV3와 비슷하다.



생산은 2026년 2분기 터키 공장에서 시작되며, 유럽 시장에 먼저 출시될 예정이다. 배터리는 58.3kWh와 81.4kWh 두 가지 옵션으로 구성되며, 각각 약 418km와 587km 주행이 가능할 것으로 보인다.



예상 가격은 약 4600만원 수준으로 아이오닉5보다 낮게 책정돼, 접근성을 높인 전략으로 분석된다. 현대차는 “플레오스 커넥트가 적용된 첫 차량으로 2026년 아이오닉3가 출시될 예정”이라고 밝혔다.

# core/platforms/base_naver_blog.py

# 버전: 1.2.0 - 캡션 찾기 로직 개선

# 최종 업데이트: 2025-01-24

# 주요 기능:

# - figcaption 우선 캡션 추출 로직 개선

# - 캡션 찾기 우선순위 명확화 (figcaption → alt 속성)

# - 디버그 로그 추가로 캡션 추출 과정 추적 가능

# - 클립보드 붙여넣기 방식의 캡션 입력 강화

# - 이미지 클릭 및 활성화 로직 개선

# - 더 안정적인 텍스트 입력을 위한 span 요소 타겟팅

# - Media CMS naver_text 자동 입력 (본문 + 소제목)

# - HTML 구조 파싱을 통한 자연스러운 타이핑 시뮬레이션

# - 소제목 자동 포맷팅 (19px 굵게)

# - 전체 콘텐츠 구조적 파싱 및 5줄마다 이미지 자동 배치

# - 완전 자동화된 네이버 블로그 포스팅 시스템

# - 간소화된 4단계 포스팅 플로우 (제목 → 전체 콘텐츠 삽입)

# - 첫 번째 이미지부터 체계적인 콘텐츠 삽입 시스템

# - 박스형 썸네일 6개 자동 추가 (링크 미리보기)

# - 셀렉터 대기시간 최적화 (5초 → 2초)

# - URL 입력 전 기존 내용 완전 삭제

# - URL 처리 대기시간 최적화 (5초)


import os

import sys

import time

import logging

import pyperclip

from datetime import datetime, timedelta

from typing import List

from selenium.webdriver.common.by import By

from selenium.webdriver.common.keys import Keys

from selenium.webdriver.common.action_chains import ActionChains

from selenium.webdriver.support.ui import WebDriverWait

from selenium.webdriver.support import expected_conditions as EC

from selenium.common.exceptions import TimeoutException, NoSuchElementException, NoSuchWindowException


from core.platforms.base_platform import BasePlatform, PostingResult


class BaseNaverBlogPlatform(BasePlatform):

def __init__(self, site_name: str, site_config: dict, chrome_manager):

self.chrome_manager = chrome_manager

self.site_name = site_name

self.site_config = site_config

driver = chrome_manager.get_or_create_driver('naver_blog')

if not driver:

raise Exception("네이버 블로그 드라이버를 가져올 수 없습니다")

airtable_manager = self._get_airtable_manager(site_name, site_config)

super().__init__(driver, site_config, airtable_manager, "naver_blog")

# � 이미지 다운로드 임시 폴더 설정

from pathlib import Path

self.temp_upload_dir = Path(__file__).parent.parent.parent / "temp_naver_uploads"

self.temp_upload_dir.mkdir(exist_ok=True)

self._setup_logger()

self._load_naver_blog_settings_from_env()

self.tabs = {}

self.current_tab = None

self.logger.info(f"✅ {site_name} 네이버 블로그 플랫폼 초기화 완료")

self.logger.info(f"� 이미지 임시 폴더: {self.temp_upload_dir}")


def _setup_logger(self):

self.logger.handlers.clear()

self.logger.propagate = False

console_handler = logging.StreamHandler()

log_format = self.site_config.get('logging', {}).get('format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')

formatter = logging.Formatter(log_format)

console_handler.setFormatter(formatter)

console_handler.setLevel(logging.INFO)

self.logger.addHandler(console_handler)

self.logger.setLevel(logging.INFO)


def _load_naver_blog_settings_from_env(self):

import os

from dotenv import load_dotenv

env_file = self.site_config.get('airtable', {}).get('env_file', f"env/{self.site_name}.env")

if os.path.exists(env_file):

load_dotenv(env_file, override=True)

self.logger.info(f"� 환경변수 파일 로드: {env_file}")


if self.site_name == "withcar":

env_prefix = "WITHCAR"

elif self.site_name == "econmingle":

env_prefix = "ECONMINGLE"

elif self.site_name == "kookbangtimes":

env_prefix = "KOOKBANG"

else:

env_prefix = self.site_name.upper()

# 네이버 블로그 설정

self.target_url = self.platform_config.get('target_url', 'https://section.blog.naver.com/BlogHome.naver')

self.naver_id = os.getenv(f"{env_prefix}_NAVER_ID", "")

self.naver_password = os.getenv(f"{env_prefix}_NAVER_PASSWORD", "")

self.manual_login_wait = self.platform_config.get('manual_login_wait', 60)

self.logger.info(f"✅ 네이버 블로그 설정 로드 완료")

self.logger.info(f"� 타겟 URL: {self.target_url}")


def _get_airtable_manager(self, site_name: str, site_config: dict):

try:

module_name = f"sites.{site_name}.{site_name}_airtable"

airtable_module = __import__(module_name, fromlist=['AirtableManager'])

AirtableManager = getattr(airtable_module, 'AirtableManager')

return AirtableManager(site_config)

except Exception as e:

self.logger.error(f"❌ {site_name} Airtable 관리자 로드 실패: {e}")

return None


def get_platform_specific_urls(self) -> dict:

return {

'login_url': 'https://nid.naver.com/nidlogin.login',

'main_url': self.target_url,

'write_url': 'https://blog.naver.com/PostWriteForm.naver'

}


def login(self) -> bool:

self.logger.info("▶️ Naver Blog 플랫폼 초기화 및 로그인 시퀀스 시작...")

if not self.open_tabs():

self.logger.error("❌ 탭 생성에 실패하여 시퀀스를 중단합니다.")

return False

return self.perform_login()


def open_tabs(self) -> bool:

"""네이버 블로그 전용 2개 탭 사용"""

try:

current_handles = self.driver.window_handles

self.logger.info(f"� 현재 브라우저 탭 수: {len(current_handles)}개")


if self.tabs and len(self.tabs) == 2 and self._are_tabs_valid():

self.logger.info("✅ 기존 2개 탭이 유효하여 재사용합니다.")

self.switch_to_tab('main')

return True

self.tabs.clear()

# 2개 탭만 필요

if len(current_handles) < 2:

self.driver.execute_script("window.open('about:blank', '_blank');")

time.sleep(0.5)

# 추가 탭이 있으면 정리

if len(current_handles) > 2:

self._close_extra_tabs()

current_handles = self.driver.window_handles


all_handles = self.driver.window_handles

self.tabs = {

'main': all_handles[0], # 네이버 블로그 메인

'content': all_handles[1] # 워드프레스 (이미지 다운로드용)

}

self.logger.info(f"✅ 2개 탭 구성 완료: {list(self.tabs.keys())}")

self.switch_to_tab('main')

self.driver.get(self.target_url)

self.logger.info(f"� 메인 탭에서 타겟 URL로 이동: {self.target_url}")

return True

except Exception as e:

self.logger.error(f"❌ 탭 생성 실패: {e}", exc_info=True)

return False


def switch_to_tab(self, tab_name: str) -> bool:

try:

if tab_name not in self.tabs:

self.logger.error(f"❌ '{tab_name}' 탭이 존재하지 않습니다.")

return False

self.driver.switch_to.window(self.tabs[tab_name])

self.current_tab = tab_name

time.sleep(3.0)

return True

except NoSuchWindowException:

self.logger.error(f"❌ {tab_name} 탭이 닫혔습니다. 탭을 재생성합니다.")

self.tabs.clear()

return self.open_tabs()

except Exception as e:

self.logger.error(f"❌ {tab_name} 탭 전환 실패: {e}", exc_info=True)

return False


def _are_tabs_valid(self) -> bool:

try:

if not self.tabs or len(self.tabs) != 2:

return False

current_handles = self.driver.window_handles

for handle in self.tabs.values():

if handle not in current_handles:

return False

return True

except Exception:

return False


def _close_extra_tabs(self):

try:

current_handles = self.driver.window_handles

if len(current_handles) <= 2:

return

main_handle = self.tabs.get('main', current_handles[0])

content_handle = self.tabs.get('content', current_handles[1])

handles_to_keep = {main_handle, content_handle}


for handle in current_handles:

if handle not in handles_to_keep:

self.driver.switch_to.window(handle)

self.driver.close()

self.switch_to_tab('main')

except Exception as e:

self.logger.warning(f"⚠️ 추가 탭 정리 중 오류: {e}")



def get_current_tab_info(self) -> dict:

return {

'current_tab': self.current_tab,

'available_tabs': list(self.tabs.keys()),

'total_tabs': len(self.tabs)

}


def _get_copy_key(self):

return Keys.COMMAND if sys.platform == 'darwin' else Keys.CONTROL


def _find_main_content_element(self):

self.logger.info("원본 글에서 본문 영역을 찾는 중...")

selectors = [

'article .content', 'article .post-content', '.entry-content',

'.post-content', 'article > div > div', '[id^="post-"] > div > div'

]

for selector in selectors:

try:

element = self.driver.find_element(By.CSS_SELECTOR, selector)

if element and element.text.strip() and len(element.text.strip()) > 50:

self.logger.info(f"✅ 본문 요소 발견 (셀렉터: {selector})")

return element

except NoSuchElementException:

continue

self.logger.error("❌ 모든 셀렉터를 시도했지만 본문 요소를 찾지 못했습니다.")

return None


def _get_3days_ago_content_from_airtable(self) -> dict:

"""[수정] 1일 전, 사이트별 카테고리, naver_blog가 False인 데이터를 찾습니다."""

try:

# 사이트별 카테고리 설정

if self.site_name.lower() == 'withcar':

target_categories = ['newcar']

elif self.site_name.lower() == 'econmingle':

target_categories = ['economy', 'realestate'] # ← 두 개 카테고리 지원

elif self.site_name.lower() == 'reportera':

target_categories = ['news']

else:

target_categories = ['newcar'] # 기본값

self.logger.info(f"� Airtable에서 1일 전 {target_categories} 카테고리 중 미발행 콘텐츠 조회를 시작합니다...")

target_date = datetime.now() - timedelta(days=1)

self.logger.info(f"� 목표 날짜: {target_date.strftime('%Y-%m-%d')}, 카테고리: {target_categories}, naver_blog: False")

all_records = self.airtable_manager.table.all(

max_records=100,

sort=["-created_time"]

)

if not all_records:

self.logger.warning("❌ Airtable에서 조회된 레코드가 없습니다.")

return {}


self.logger.info(f"� 총 {len(all_records)}개 레코드에서 조건에 맞는 데이터 검색 중...")


best_match = None

min_date_diff = float('inf')


for record in all_records:

fields = record.get('fields', {})

# [수정] 여러 카테고리 중 하나라도 일치하고 + 'naver_blog' 필드가 True가 아니어야 함

record_category = fields.get('category')

if record_category not in target_categories or fields.get('naver_blog') is True:

continue

created_time_str = record.get('createdTime', '')

if not created_time_str:

continue

created_time = datetime.fromisoformat(created_time_str.replace('Z', '+00:00'))

date_diff = abs((created_time.date() - target_date.date()).days)

if date_diff < min_date_diff:

min_date_diff = date_diff

best_match = record

if best_match:

content_data = best_match.get('fields', {})

content_data['id'] = best_match.get('id')

actual_date_str = best_match.get('createdTime', '알수없음').split('T')[0]

actual_category = content_data.get('category', '알수없음')

self.logger.info(f"✅ 조건에 맞는 콘텐츠를 찾았습니다. (실제 날짜: {actual_date_str}, 카테고리: {actual_category}, 차이: {min_date_diff}일)")

return content_data

else:

self.logger.warning(f"❌ 조건(1일 전, {target_categories}, 미발행)에 맞는 콘텐츠를 찾지 못했습니다.")

return {}

except Exception as e:

self.logger.error(f"❌ 1일 전 콘텐츠 조회 중 오류 발생: {e}", exc_info=True)

return {}


def _copy_content_from_wp_link_3days(self, url: str) -> bool:

self.logger.info(f"� 1일 전 콘텐츠({url}) 추출 및 복사를 시작합니다...")

if not self.switch_to_tab('content'):

self.logger.error("❌ 'content' 탭으로 전환 실패")

return False

try:

self.driver.get(url)

WebDriverWait(self.driver, 15).until(lambda d: d.execute_script('return document.readyState') == 'complete')

time.sleep(2)


content_element = self._find_main_content_element()

if not content_element:

return False


self.logger.info("� 본문에서 AI 관련 요소를 제거합니다...")

self.driver.execute_script("""

var container = arguments[0];

var aiSelectors = ['.ai-summary-box', '.ai-quiz-box', '.ai-faq-box', '.ai-chatbot-box', '#aifaq-poll-results', '.ssb-container'];

var removedCount = 0;

aiSelectors.forEach(function(selector) {

var elements = container.querySelectorAll(selector);

elements.forEach(function(el) { el.remove(); removedCount++; });

});

console.log('제거된 AI 요소 수: ' + removedCount);

""", content_element)


self.driver.execute_script("""

var container = arguments[0];

var selection = window.getSelection();

selection.removeAllRanges();

var range = document.createRange();

range.selectNodeContents(container);

selection.addRange(range);

""", content_element)

time.sleep(1)


copy_key = self._get_copy_key()

ActionChains(self.driver).key_down(copy_key).send_keys('c').key_up(copy_key).perform()

time.sleep(1)

self.logger.info("✅ 클립보드에 (AI가 제거된) 1일 전 본문 내용 복사 완료")

return True

except Exception as e:

self.logger.error(f"❌ 1일 전 콘텐츠 복사 중 오류 발생: {e}", exc_info=True)

return False


def _copy_naver_text_to_clipboard(self, naver_text: str) -> bool:

"""Media CMS의 naver_text를 클립보드에 복사"""

try:

if not naver_text:

self.logger.error("❌ naver_text가 비어있습니다")

return False

self.logger.info(f"� Media CMS naver_text 클립보드 복사 중... (길이: {len(naver_text)}자)")

# pyperclip을 사용하여 클립보드에 복사

pyperclip.copy(naver_text)

time.sleep(1)

self.logger.info("✅ naver_text 클립보드 복사 완료")

return True

except Exception as e:

self.logger.error(f"❌ naver_text 복사 중 오류 발생: {e}", exc_info=True)

return False


def check_login_status(self) -> bool:

self.logger.info("� 네이버 블로그 로그인 상태를 정확하게 확인합니다...")

self.switch_to_tab('main')

if self.target_url not in self.driver.current_url:

self.driver.get(self.target_url)

time.sleep(2)

try:

button_selector = "#container > div > aside > div > div:nth-child(1) > nav > a:nth-child(2)"

login_area_button = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located((By.CSS_SELECTOR, button_selector)))

if '글쓰기' in login_area_button.text:

self.logger.info("✅ '글쓰기' 버튼을 확인하여 로그인 상태로 판단합니다.")

return True

return False

except Exception:

return False

def _handle_manual_login(self) -> bool:

self.logger.info(f"� {self.manual_login_wait}초 내에 수동으로 로그인해주세요.")

try:

WebDriverWait(self.driver, self.manual_login_wait).until(lambda driver: 'nid.naver.com' not in driver.current_url)

return True

except TimeoutException:

return False

def _handle_automatic_login(self) -> bool:

"""네이버 자동 로그인 (개선 버전)"""

if not self.naver_id or not self.naver_password:

self.logger.warning("⚠️ 네이버 ID 또는 비밀번호가 설정되지 않았습니다.")

return False

try:

self.logger.info("� 네이버 자동 로그인 시도 중...")

# ID 입력

id_input = WebDriverWait(self.driver, 10).until(

EC.presence_of_element_located((By.CSS_SELECTOR, "input#id"))

)

id_input.click()

time.sleep(0.5)

# 직접 입력 (클립보드 사용 안 함)

id_input.clear()

for char in self.naver_id:

id_input.send_keys(char)

time.sleep(0.1) # 사람처럼 천천히 입력

time.sleep(1)

# 비밀번호 입력

pw_input = self.driver.find_element(By.CSS_SELECTOR, "input#pw")

pw_input.click()

time.sleep(0.5)

pw_input.clear()

for char in self.naver_password:

pw_input.send_keys(char)

time.sleep(0.1)

time.sleep(1)

# 로그인 버튼 클릭

self.logger.info("✅ ID/PW 입력 완료, 로그인 버튼 클릭 중...")

login_button = self.driver.find_element(By.CSS_SELECTOR, "button#log\\.login")

login_button.click()

# 로그인 완료 대기

try:

WebDriverWait(self.driver, 15).until(

lambda driver: 'nid.naver.com' not in driver.current_url

)

self.logger.info("✅ 네이버 자동 로그인 성공!")

return True

except TimeoutException:

self.logger.error("❌ 로그인 후 페이지 전환 실패 (캡차 or 보안 문제 가능성)")

return False

except TimeoutException as e:

self.logger.error(f"❌ 로그인 페이지 요소를 찾을 수 없습니다: {e}")

return False

except Exception as e:

self.logger.error(f"❌ 자동 로그인 중 오류 발생: {e}", exc_info=True)

return False


def perform_login(self) -> bool:

if self.check_login_status():

naver_login_success = True

else:

naver_login_success = self._perform_naver_login()

return naver_login_success


def _perform_naver_login(self) -> bool:

self.switch_to_tab('main')

if self.target_url not in self.driver.current_url: self.driver.get(self.target_url)

try:

login_link = WebDriverWait(self.driver, 15).until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#container > div > aside > div > div:nth-child(1) > div.area_signin > div > a")))

login_link.click()

WebDriverWait(self.driver, 10).until(EC.url_contains("nid.naver.com"))

except Exception:

return False

if self._handle_automatic_login() or self._handle_manual_login():

time.sleep(2)

return self.check_login_status()

return False



def perform_posting(self, content_data: dict) -> PostingResult:

"""전체 포스팅 실행 (Naver CMS 사용)"""

try:

self.logger.info("� 네이버 블로그 포스팅 시작 (Naver CMS 조회)...")

# ✅ 1. Naver CMS에서 데이터 가져오기 (간단하게!)

naver_cms_info = self.airtable_manager.get_naver_cms_item_random()

if not naver_cms_info:

self.logger.error("❌ Naver CMS에서 조건에 맞는 레코드를 찾을 수 없습니다")

return PostingResult(success=False, message="Naver CMS 레코드 없음")

# ✅ 2. 필수 필드 체크 (4개 모두 필수!)

required_fields = ['naver_title', 'naver_text', 'naver_category', 'naver_tag']

missing_fields = []

for field in required_fields:

field_value = naver_cms_info.get(field)

# 빈 문자열도 체크

if not field_value or (isinstance(field_value, str) and not field_value.strip()):

missing_fields.append(field)

if missing_fields:

self.logger.error(f"❌ Naver CMS 필수 필드 누락: {', '.join(missing_fields)}")

# ✨ 실패해도 status를 'Done'으로 업데이트 (다음 사이클에서 건너뛰기)

record_id = naver_cms_info.get('record_id')

if record_id:

try:

self.airtable_manager.naver_cms_table.update(record_id, {'status': 'Done'})

self.logger.warning("⚠️ Naver CMS 필수 필드 누락으로 status='Done' 업데이트 (건너뛰기)")

except Exception as e:

self.logger.error(f"❌ Naver CMS status 업데이트 실패: {e}")

return PostingResult(

success=False,

message=f"Naver CMS 필수 필드 누락: {', '.join(missing_fields)}"

)

# ✅ 3. 필드 추출

naver_title = naver_cms_info.get('naver_title', '').strip()

naver_text = naver_cms_info.get('naver_text', '').strip()

naver_category = naver_cms_info.get('naver_category', '').strip()

naver_tag = naver_cms_info.get('naver_tag')

wp_link = naver_cms_info.get('wp_link', '').strip()

record_id = naver_cms_info.get('record_id', '')

if not wp_link:

self.logger.error("❌ 워드프레스 URL이 없습니다")

return PostingResult(success=False, message="워드프레스 URL 없음")

self.logger.info("✅ Naver CMS 필수 필드 검증 완료")

self.logger.info(f" - naver_title: {naver_title[:30]}...")

self.logger.info(f" - naver_text: {len(naver_text)}자")

self.logger.info(f" - naver_category: {naver_category}")

self.logger.info(f" - naver_tag: {naver_tag if isinstance(naver_tag, str) else f'{len(naver_tag)}개'}")

self.logger.info(f" - wp_link: {wp_link[:50]}...")

# ✅ 4. 네이버 블로그 글쓰기 창 열기 (제목만 입력)

self.logger.info("� 네이버 블로그 글쓰기 창 열기...")

naver_write_window = self._open_naver_blog_write_window(naver_title)

if not naver_write_window:

return PostingResult(success=False, message="글쓰기 창 열기 실패")

# ✅ 5. 전체 콘텐츠 및 모든 이미지 처리 (첫 번째 이미지 포함)

self.logger.info("� 전체 콘텐츠 및 이미지 삽입 시작...")

# record_id 대신 naver_cms_info 전체를 전달 (naver_text 포함)

if not self._insert_full_content_with_images_from_naver_cms(

naver_write_window,

naver_cms_info,

wp_link

):

self.logger.warning("⚠️ 전체 콘텐츠 삽입 중 일부 오류 발생")

else:

self.logger.info("� 전체 포스팅 완료!")

# ✅ 6. 박스형 썸네일 추가 (6개)

self.logger.info("� 박스형 썸네일 추가 (6개)...")

success_count = 0

for i in range(6):

self.logger.info(f"� 박스형 썸네일 {i+1}번째 추가...")

if self._add_box_thumbnail_to_naver(naver_write_window, record_index=i, current_wp_url=wp_link):

success_count += 1

self.logger.info(f"✅ 박스형 썸네일 {i+1}번째 추가 성공")

if i < 5:

self.logger.info("⬆️ 커서를 위로 한 칸 이동...")

ActionChains(self.driver).send_keys(Keys.ARROW_UP).perform()

time.sleep(0.5)

else:

self.logger.warning(f"⚠️ 박스형 썸네일 {i+1}번째 추가 실패")

break

self.logger.info(f"✅ 박스형 썸네일 {success_count}/6개 추가 완료")

# ✅ 7. 네이버 블로그 발행

self.logger.info("� 네이버 블로그 발행...")

if self._publish_naver_post_with_naver_cms(naver_write_window, naver_cms_info):

self.logger.info("✅ 네이버 블로그 발행 완료")

else:

self.logger.warning("⚠️ 네이버 블로그 발행 실패")

return PostingResult(success=True, message="전체 포스팅 완료")

except Exception as e:

self.logger.error(f"❌ 포스팅 실패: {e}", exc_info=True)

return PostingResult(success=False, message=f"포스팅 실패: {e}")


def _open_naver_blog_write_window(self, title: str):

try:

if not self.switch_to_tab('main'): return None

write_button = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#container > div > aside > div > div:nth-child(1) > nav > a:nth-child(2)")))

write_button.click()

WebDriverWait(self.driver, 10).until(EC.number_of_windows_to_be(3))

new_window = [w for w in self.driver.window_handles if w not in self.tabs.values()][0]

self.driver.switch_to.window(new_window)

WebDriverWait(self.driver, 10).until(EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame")))

try:

cancel_button = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.se-popup-button-cancel")))

cancel_button.click(); time.sleep(3)

except TimeoutException:

pass

title_input = WebDriverWait(self.driver, 15).until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.se-title-text")))


# 1. 포커스를 주고 클릭

ActionChains(self.driver).move_to_element(title_input).click().perform()

time.sleep(0.5)


# 2. 기존 내용 제거 (전체 선택 후 삭제)

modifier = Keys.COMMAND if sys.platform == 'darwin' else Keys.CONTROL

ActionChains(self.driver).key_down(modifier).send_keys('a').key_up(modifier).send_keys(Keys.DELETE).perform()

time.sleep(0.5)


# 3. 제목을 0.05초 간격으로 사람처럼 타이핑

self.logger.info(f"⌨️ 제목 '{title}'을 0.05초 간격으로 타이핑 중...")

for char in title:

ActionChains(self.driver).send_keys(char).perform()

time.sleep(0.05) # 요청하신 지연 시간 (0.05초)


# 4. 입력 완료 (Enter)

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

return new_window

except Exception:

return None



def _add_links_to_naver_blog(self, naver_write_window: str) -> bool:

try:

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

WebDriverWait(self.driver, 5).until(EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame")))

latest_records = self.airtable_manager.table.all(max_records=6, sort=["-created_time"])

if len(latest_records) < 6: return False

urls = [rec.get('fields', {}).get('wp_link') for rec in reversed(latest_records)]

modifier = self._get_copy_key()

ActionChains(self.driver).key_down(modifier).send_keys(Keys.END).key_up(modifier).perform(); time.sleep(0.5)

ActionChains(self.driver).send_keys(Keys.ENTER).send_keys(Keys.ENTER).perform(); time.sleep(0.5)

for idx, url in enumerate(urls, start=1):

ActionChains(self.driver).send_keys(url).perform(); time.sleep(3)

ActionChains(self.driver).send_keys(Keys.ENTER).perform(); time.sleep(3)

ac = ActionChains(self.driver)

for _ in range(2 if idx == 1 else 1): ac = ac.send_keys(Keys.ARROW_UP)

ac.perform(); time.sleep(0.5)

ActionChains(self.driver).send_keys(Keys.HOME).key_down(Keys.SHIFT).send_keys(Keys.END).key_up(Keys.SHIFT).perform(); time.sleep(0.3)

ActionChains(self.driver).send_keys(Keys.DELETE).perform(); time.sleep(0.5)

if idx == len(urls):

ActionChains(self.driver).send_keys(Keys.DELETE).perform(); time.sleep(0.5)

time.sleep(1.5)

return True

except Exception:

return False




def _add_tags_to_naver_blog(self, naver_write_window: str, tags: List[str]) -> bool:

"""네이버 블로그에 태그 추가"""

try:

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

WebDriverWait(self.driver, 5).until(EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame")))

self.logger.info(f"�️ 태그 {len(tags)}개 추가 중...")

# 태그 입력 필드 찾기 (실제 셀렉터는 네이버 블로그 구조에 맞게 수정 필요)

tag_input = WebDriverWait(self.driver, 10).until(

EC.element_to_be_clickable((By.CSS_SELECTOR, "input[placeholder*='태그']"))

)

for tag in tags:

tag_input.click()

tag_input.send_keys(tag)

tag_input.send_keys(Keys.ENTER) # 또는 쉼표

time.sleep(0.5)

self.logger.info(f" - 태그 추가: {tag}")

self.logger.info("✅ 태그 추가 완료")

return True

except Exception as e:

self.logger.error(f"❌ 태그 추가 실패: {e}", exc_info=True)

return False


def _publish_naver_post(self, naver_write_window: str, record_id: str = None, category: str = '[POST] 더위드카') -> bool:

"""네이버 블로그 발행 (카테고리 설정 포함)"""

try:

self.logger.info(f" - Step 6: 네이버 블로그 글 최종 발행 (카테고리: {category})")

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

time.sleep(1)


# 1단계: 발행하기 버튼 클릭

self.logger.info(" - Step 6-1: 발행하기 버튼 클릭")

publish_button_selectors = [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > button > span"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/button/span"),

(By.CSS_SELECTOR, "button[data-click-area='tpb.publish']"), # 백업 셀렉터

]

publish_button_found = False

for by, selector in publish_button_selectors:

try:

publish_button = WebDriverWait(self.driver, 5).until(

EC.element_to_be_clickable((by, selector))

)

publish_button.click()

time.sleep(2)

publish_button_found = True

self.logger.info("✅ 발행하기 버튼 클릭 성공")

break

except TimeoutException:

continue

if not publish_button_found:

self.logger.error("❌ 발행하기 버튼을 찾을 수 없습니다")

return False


# 2단계: 카테고리 설정 (기본값 사용)

self.logger.info(" - Step 6-2: 카테고리 설정")

# Media CMS는 더 이상 사용하지 않음 - 기본값 사용

naver_category = None

self.logger.warning(" - ⚠️ Media CMS는 더 이상 사용하지 않음, 기본 카테고리 사용")

# 카테고리 매핑 (naver_category → 실제 카테고리)

category_mapping = {

'신차소식': '신차소식',

'자동차 뉴스': '자동차 뉴스',

'자동차 이슈': '자동차 이슈',

'자동차 상식': '자동차 상식'

}

target_category = category_mapping.get(naver_category, '신차소식') # 기본값: 신차소식

self.logger.info(f" - 설정할 카테고리: {target_category}")

try:

# 카테고리 토글 버튼 클릭

self.logger.info(" - 카테고리 토글 버튼 클릭")

category_toggle_selectors = [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > button"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/button"),

(By.CSS_SELECTOR, "button[data-click-area='tpb*i.category']"), # 백업 셀렉터

]

category_toggle_found = False

for by, selector in category_toggle_selectors:

try:

category_toggle = WebDriverWait(self.driver, 3).until(

EC.element_to_be_clickable((by, selector))

)

category_toggle.click()

time.sleep(1)

category_toggle_found = True

self.logger.info(" - ✅ 카테고리 토글 버튼 클릭 성공")

break

except TimeoutException:

continue

if not category_toggle_found:

self.logger.warning(" - ⚠️ 카테고리 토글 버튼을 찾을 수 없음")

else:

# 카테고리 선택

self.logger.info(f" - 카테고리 '{target_category}' 선택")

category_selectors = {

'신차소식': [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > div:nth-child(3) > div > ul > li:nth-child(1) > span > label"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/div[2]/div/ul/li[1]/span/label")

],

'자동차 뉴스': [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > div:nth-child(3) > div > ul > li:nth-child(2) > span > label"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/div[2]/div/ul/li[2]/span/label")

],

'자동차 이슈': [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > div:nth-child(3) > div > ul > li:nth-child(3) > span > label"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/div[2]/div/ul/li[3]/span/label")

],

'자동차 상식': [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > div:nth-child(3) > div > ul > li:nth-child(4) > span > label"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/div[2]/div/ul/li[4]/span/label")

]

}

category_option_found = False

if target_category in category_selectors:

for by, selector in category_selectors[target_category]:

try:

category_option = WebDriverWait(self.driver, 3).until(

EC.element_to_be_clickable((by, selector))

)

category_option.click()

time.sleep(1)

category_option_found = True

self.logger.info(f" - ✅ 카테고리 '{target_category}' 선택 완료")

break

except TimeoutException:

continue

if not category_option_found:

self.logger.warning(f" - ⚠️ 카테고리 '{target_category}' 선택 실패")

except Exception as e:

self.logger.warning(f" - ⚠️ 카테고리 설정 실패: {e}")


# 3단계: 태그 입력

self.logger.info(" - Step 6-3: 태그 입력")

# Media CMS는 더 이상 사용하지 않음 - 태그 없음

tags = []

self.logger.warning(" - ⚠️ Media CMS는 더 이상 사용하지 않음, 태그 입력 건너뜀")

if tags:

try:

# 태그 입력창 클릭

self.logger.info(" - 태그 입력창 클릭")

tag_input_selectors = [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div:nth-child(6) > div.option_tag__M5E_f > div > div > div"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[6]/div[2]/div/div/div")

]

tag_input_found = False

for by, selector in tag_input_selectors:

try:

tag_input = WebDriverWait(self.driver, 3).until(

EC.element_to_be_clickable((by, selector))

)

tag_input.click()

time.sleep(1)

tag_input_found = True

self.logger.info(" - ✅ 태그 입력창 클릭 성공")

break

except TimeoutException:

continue

if not tag_input_found:

self.logger.warning(" - ⚠️ 태그 입력창을 찾을 수 없음")

else:

# 각 태그를 하나씩 입력

for i, tag in enumerate(tags):

if tag and tag.strip(): # 빈 태그 제외

self.logger.info(f" - 태그 {i+1}/{len(tags)} 입력: {tag}")

# 태그 입력 (전체 태그를 한 번에)

ActionChains(self.driver).send_keys(tag.strip()).perform()

time.sleep(0.5)

# Enter 키 입력

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.5)

self.logger.info(f" - ✅ 태그 '{tag}' 입력 완료")

self.logger.info(f" - ✅ 총 {len(tags)}개 태그 입력 완료")

except Exception as e:

self.logger.warning(f" - ⚠️ 태그 입력 실패: {e}")

else:

self.logger.info(" - 태그가 없어 입력 건너뜀")


# 4단계: 최종 발행

self.logger.info(" - Step 6-4: 최종 발행")

try:

# 최종 발행 버튼 클릭

self.logger.info(" - 최종 발행 버튼 클릭")

final_publish_selectors = [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.layer_btn_area__UzyKH > div > button"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[8]/div/button"),

(By.CSS_SELECTOR, "button[data-click-area='tpb*i.publish']"), # 백업 셀렉터

]

final_publish_found = False

for by, selector in final_publish_selectors:

try:

final_publish_button = WebDriverWait(self.driver, 5).until(

EC.element_to_be_clickable((by, selector))

)

final_publish_button.click()

time.sleep(5)

final_publish_found = True

self.logger.info(" - ✅ 최종 발행 버튼 클릭 성공")

break

except TimeoutException:

continue

if not final_publish_found:

self.logger.error(" - ❌ 최종 발행 버튼을 찾을 수 없습니다")

return False

self.logger.info("� 포스팅 발행 성공!")

# Share Status의 naver_blog 필드 체크 완료로 업데이트

if record_id:

try:

self.logger.info("� Share Status naver_blog 필드 체크 완료로 업데이트...")

update_data = {

'naver_blog': True

}

self.airtable_manager.table.update(record_id, update_data)

self.logger.info("✅ Share Status naver_blog 필드 업데이트 완료")

except Exception as e:

self.logger.warning(f"⚠️ Share Status 업데이트 실패: {e}")

else:

self.logger.warning("⚠️ record_id가 없어 Share Status 업데이트 건너뜀")

try:

self.driver.close()

self.switch_to_tab('main')

except Exception:

pass

except Exception as e:

self.logger.error(f" - ❌ 최종 발행 실패: {e}")

return False

return True


except Exception as e:

self.logger.error(f"❌ 발행 실패: {e}", exc_info=True)

return False


def _publish_naver_post_with_naver_cms(

self,

naver_write_window: str,

naver_cms_info: dict

) -> bool:

"""

Naver CMS 데이터를 사용하여 네이버 블로그 발행

(기존 _publish_naver_post 메서드 수정)

"""

try:

self.logger.info("� 네이버 블로그 발행 (Naver CMS 사용)...")

# naver_cms_info에서 필요한 정보 추출

record_id = naver_cms_info.get('record_id')

naver_category = naver_cms_info.get('naver_category', '').strip()

naver_tag = naver_cms_info.get('naver_tag', [])

# naver_tag가 문자열인 경우 리스트로 변환

if isinstance(naver_tag, str):

tags = [tag.strip() for tag in naver_tag.split(',') if tag.strip()]

else:

tags = naver_tag if isinstance(naver_tag, list) else []

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

time.sleep(1)


# 1단계: 발행하기 버튼 클릭

self.logger.info(" - Step 6-1: 발행하기 버튼 클릭")

publish_button_selectors = [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > button > span"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/button/span"),

(By.CSS_SELECTOR, "button[data-click-area='tpb.publish']"), # 백업 셀렉터

]

publish_button_found = False

for by, selector in publish_button_selectors:

try:

publish_button = WebDriverWait(self.driver, 5).until(

EC.element_to_be_clickable((by, selector))

)

publish_button.click()

time.sleep(2)

publish_button_found = True

self.logger.info("✅ 발행하기 버튼 클릭 성공")

break

except TimeoutException:

continue

if not publish_button_found:

self.logger.error("❌ 발행하기 버튼을 찾을 수 없습니다")

return False


# 2단계: Naver CMS에서 naver_category 가져와서 카테고리 설정

self.logger.info(" - Step 6-2: Naver CMS에서 naver_category 가져오기")

if naver_category:

self.logger.info(f" - ✅ Naver CMS에서 가져온 naver_category: {naver_category}")

else:

self.logger.warning(" - ⚠️ Naver CMS에서 naver_category를 찾을 수 없음, 기본값 사용")

# 카테고리 매핑 (naver_category → 실제 카테고리)

category_mapping = {

'신차소식': '신차소식',

'자동차 뉴스': '자동차 뉴스',

'자동차 이슈': '자동차 이슈',

'자동차 상식': '자동차 상식'

}

target_category = category_mapping.get(naver_category, '신차소식') # 기본값: 신차소식

self.logger.info(f" - 설정할 카테고리: {target_category}")

try:

# 카테고리 토글 버튼 클릭

self.logger.info(" - 카테고리 토글 버튼 클릭")

category_toggle_selectors = [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > button"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/button"),

(By.CSS_SELECTOR, "button[data-click-area='tpb*i.category']"), # 백업 셀렉터

]

category_toggle_found = False

for by, selector in category_toggle_selectors:

try:

category_toggle = WebDriverWait(self.driver, 3).until(

EC.element_to_be_clickable((by, selector))

)

category_toggle.click()

time.sleep(1)

category_toggle_found = True

self.logger.info(" - ✅ 카테고리 토글 버튼 클릭 성공")

break

except TimeoutException:

continue

if not category_toggle_found:

self.logger.warning(" - ⚠️ 카테고리 토글 버튼을 찾을 수 없음")

else:

# 카테고리 선택

self.logger.info(f" - 카테고리 '{target_category}' 선택")

category_selectors = {

'신차소식': [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > div:nth-child(3) > div > ul > li:nth-child(1) > span > label"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/div[2]/div/ul/li[1]/span/label")

],

'자동차 뉴스': [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > div:nth-child(3) > div > ul > li:nth-child(2) > span > label"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/div[2]/div/ul/li[2]/span/label")

],

'자동차 이슈': [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > div:nth-child(3) > div > ul > li:nth-child(3) > span > label"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/div[2]/div/ul/li[3]/span/label")

],

'자동차 상식': [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.option_category___kpJc > div > div > div:nth-child(3) > div > ul > li:nth-child(4) > span > label"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[1]/div/div/div[2]/div/ul/li[4]/span/label")

]

}

category_option_found = False

if target_category in category_selectors:

for by, selector in category_selectors[target_category]:

try:

category_option = WebDriverWait(self.driver, 3).until(

EC.element_to_be_clickable((by, selector))

)

category_option.click()

time.sleep(1)

category_option_found = True

self.logger.info(f" - ✅ 카테고리 '{target_category}' 선택 완료")

break

except TimeoutException:

continue

if not category_option_found:

self.logger.warning(f" - ⚠️ 카테고리 '{target_category}' 선택 실패")

except Exception as e:

self.logger.warning(f" - ⚠️ 카테고리 설정 실패: {e}")


# 3단계: 태그 입력

self.logger.info(" - Step 6-3: Naver CMS에서 tag 가져와서 태그 입력")

if tags:

self.logger.info(f" - ✅ Naver CMS에서 가져온 태그: {tags}")

else:

self.logger.warning(" - ⚠️ Naver CMS에서 tag를 찾을 수 없음")

if tags:

try:

# 태그 입력창 클릭

self.logger.info(" - 태그 입력창 클릭")

tag_input_selectors = [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div:nth-child(6) > div.option_tag__M5E_f > div > div > div"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[6]/div[2]/div/div/div")

]

tag_input_found = False

for by, selector in tag_input_selectors:

try:

tag_input = WebDriverWait(self.driver, 3).until(

EC.element_to_be_clickable((by, selector))

)

tag_input.click()

time.sleep(1)

tag_input_found = True

self.logger.info(" - ✅ 태그 입력창 클릭 성공")

break

except TimeoutException:

continue

if not tag_input_found:

self.logger.warning(" - ⚠️ 태그 입력창을 찾을 수 없음")

else:

# 각 태그를 하나씩 입력

for i, tag in enumerate(tags):

if tag and tag.strip(): # 빈 태그 제외

self.logger.info(f" - 태그 {i+1}/{len(tags)} 입력: {tag}")

# 태그 입력 (전체 태그를 한 번에)

ActionChains(self.driver).send_keys(tag.strip()).perform()

time.sleep(0.5)

# Enter 키 입력

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.5)

self.logger.info(f" - ✅ 태그 '{tag}' 입력 완료")

self.logger.info(f" - ✅ 총 {len(tags)}개 태그 입력 완료")

except Exception as e:

self.logger.warning(f" - ⚠️ 태그 입력 실패: {e}")

else:

self.logger.info(" - 태그가 없어 입력 건너뜀")


# 4단계: 최종 발행

self.logger.info(" - Step 6-4: 최종 발행")

try:

# 최종 발행 버튼 클릭

self.logger.info(" - 최종 발행 버튼 클릭")

final_publish_selectors = [

(By.CSS_SELECTOR, "#root > div > div.header__Ceaap > div > div.publish_btn_area__KjA2i > div:nth-child(2) > div > div > div > div.layer_btn_area__UzyKH > div > button"),

(By.XPATH, "//*[@id='root']/div/div[1]/div/div[3]/div[2]/div/div/div/div[8]/div/button"),

(By.CSS_SELECTOR, "button[data-click-area='tpb*i.publish']"), # 백업 셀렉터

]

final_publish_found = False

for by, selector in final_publish_selectors:

try:

final_publish_button = WebDriverWait(self.driver, 5).until(

EC.element_to_be_clickable((by, selector))

)

final_publish_button.click()

time.sleep(5)

final_publish_found = True

self.logger.info(" - ✅ 최종 발행 버튼 클릭 성공")

break

except TimeoutException:

continue

if not final_publish_found:

self.logger.error(" - ❌ 최종 발행 버튼을 찾을 수 없습니다")

return False

self.logger.info("� 포스팅 발행 성공!")

# ✅ 성공 시 Naver CMS status 업데이트

if record_id:

try:

self.logger.info("� Naver CMS status 필드 체크 완료로 업데이트...")

self.airtable_manager.naver_cms_table.update(record_id, {'status': 'Done'})

self.logger.info("✅ Naver CMS status 필드 업데이트 완료")

except Exception as e:

self.logger.warning(f"⚠️ Naver CMS status 업데이트 실패: {e}")

try:

self.driver.close()

self.switch_to_tab('main')

except Exception:

pass

except Exception as e:

self.logger.error(f" - ❌ 최종 발행 실패: {e}")

return False

return True


except Exception as e:

self.logger.error(f"❌ 발행 실패: {e}", exc_info=True)

return False


def _extract_wp_images_and_captions(self) -> list:

"""

현재 WP 페이지(content 탭)에서 article 태그 내 이미지와 캡션 추출

"""

try:

self.logger.info("�️ article 태그 내 이미지/캡션 추출 시작...")

image_data = []

# article 태그 찾기

try:

article = self.driver.find_element(By.CSS_SELECTOR, 'article')

self.logger.info(f"� article 태그 내 이미지 탐색 중...")

except:

self.logger.warning("⚠️ article 태그를 찾을 수 없어 전체 페이지에서 검색...")

article = self.driver.find_element(By.TAG_NAME, 'body')

# article 내부의 이미지 요소들 찾기

img_elements = article.find_elements(By.CSS_SELECTOR, 'img')

self.logger.info(f"� article 태그 내 이미지 {len(img_elements)}개 발견")

for i, img in enumerate(img_elements):

try:

src = img.get_attribute('src')

alt = img.get_attribute('alt')

# 유효한 이미지인지 확인

if src and src.startswith('http') and not any(skip in src.lower() for skip in ['logo', 'icon', 'avatar', 'button', 'emoji']):

# 캡션 찾기 (figcaption 우선)

caption = ""


# 1. figcaption 먼저 찾기 (1순위)

try:

parent = img.find_element(By.XPATH, '..')

figcaption = parent.find_element(By.CSS_SELECTOR, 'figcaption')

if figcaption.text.strip():

caption = figcaption.text.strip()

self.logger.debug(f"✓ figcaption 발견: {caption[:30]}...")

except:

pass


# 2. figcaption이 없으면 alt 속성 사용 (2순위)

if not caption:

alt = img.get_attribute('alt')

if alt and alt.strip():

caption = alt.strip()

self.logger.debug(f"✓ alt 속성 사용: {caption[:30]}...")


# 3. 둘 다 없으면 빈 문자열

if not caption:

self.logger.debug("✓ 캡션 없음")

image_info = {

'image_url': src,

'caption': caption

}

image_data.append(image_info)

self.logger.info(f" ✓ 이미지 {len(image_data)}: {src[:50]}... (캡션: {caption[:30] if caption else '없음'})")

except Exception as e:

self.logger.warning(f"⚠️ 이미지 {i+1} 처리 중 오류: {e}")

continue

self.logger.info(f"✅ article 태그 내 이미지 {len(image_data)}개 추출 완료")

return image_data

except Exception as e:

self.logger.error(f"❌ WP 이미지 추출 실패: {e}", exc_info=True)

return []


def _download_image_to_temp(self, image_url: str) -> str | None:

"""

이미지를 로컬 temp 폴더에 다운로드

Args:

image_url: 다운로드할 이미지 URL

Returns:

다운로드된 파일의 절대 경로 또는 None

"""

try:

import os

import requests

# URL에서 파일명 추출

filename = os.path.basename(image_url.split('?')[0])

# 저장 경로

save_path = self.temp_upload_dir / filename

self.logger.info(f"⬇️ 이미지 다운로드 시작: {image_url[:50]}...")

# 이미지 다운로드

response = requests.get(image_url, timeout=10, stream=True)

response.raise_for_status()

# 파일로 저장

with open(save_path, 'wb') as f:

for chunk in response.iter_content(chunk_size=8192):

f.write(chunk)

# 파일 존재 확인

if save_path.exists() and save_path.stat().st_size > 0:

self.logger.info(f"✅ 이미지 다운로드 성공: {os.path.basename(save_path)}")

return str(save_path.absolute())

else:

self.logger.error(f"❌ 다운로드된 파일이 비어있음: {save_path}")

return None

except Exception as e:

self.logger.error(f"❌ 이미지 다운로드 실패: {image_url[:50]}... - {e}")

return None


def _clean_temp_uploads(self):

"""temp_naver_uploads 폴더 정리"""

try:

if self.temp_upload_dir.exists():

for file in self.temp_upload_dir.glob('*'):

try:

file.unlink()

self.logger.debug(f"�️ 파일 삭제: {file.name}")

except Exception as e:

self.logger.warning(f"⚠️ 파일 삭제 실패: {file.name} - {e}")

self.logger.info("✅ temp 폴더 정리 완료")

except Exception as e:

self.logger.error(f"❌ temp 폴더 정리 실패: {e}")


def _click_naver_image_upload_button(self, naver_write_window: str) -> bool:

"""네이버 블로그 글쓰기 창에서 이미지 업로드 버튼 클릭"""

try:

self.logger.info("� 네이버 블로그 이미지 업로드 버튼 찾는 중...")

# 네이버 글쓰기 창으로 전환

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

# � mainFrame에 있는지 확인 후 전환

try:

# 현재 frame 확인

current_frame = self.driver.execute_script("return window.frameElement;")

if current_frame is None:

# top level이면 mainFrame으로 전환

WebDriverWait(self.driver, 10).until(

EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame"))

)

self.logger.info("✅ mainFrame으로 전환")

else:

self.logger.info("✅ 이미 iframe 내부에 있음")

except Exception as frame_error:

self.logger.warning(f"⚠️ frame 전환 오류 (계속 진행): {frame_error}")

# 오류 무시하고 계속

# 이미지 버튼 찾기

image_button_selectors = [

(By.CSS_SELECTOR, "li.se-toolbar-item-image button"),

(By.CSS_SELECTOR, "li.se-toolbar-item-image > button"),

(By.CSS_SELECTOR, "li.se-toolbar-item.se-toolbar-item-image"),

(By.XPATH, "//li[contains(@class, 'se-toolbar-item-image')]//button"),

]

for by, selector in image_button_selectors:

try:

image_button = WebDriverWait(self.driver, 5).until(

EC.element_to_be_clickable((by, selector))

)

if image_button and image_button.is_displayed():

self.logger.info(f"✅ 이미지 버튼 발견")

# 버튼 클릭

try:

image_button.click()

self.logger.info("✅ 일반 클릭 성공")

except:

self.driver.execute_script("arguments[0].click();", image_button)

self.logger.info("✅ JavaScript 클릭 성공")

time.sleep(2)

return True

except TimeoutException:

continue

self.logger.error("❌ 이미지 업로드 버튼을 찾을 수 없습니다")

return False

except Exception as e:

self.logger.error(f"❌ 이미지 버튼 클릭 실패: {e}", exc_info=True)

return False


def _upload_images_to_naver(self, naver_write_window: str, image_files: list) -> bool:

"""네이버 블로그에 이미지 업로드"""

try:

self.logger.info(f"� 네이버 블로그에 {len(image_files)}개 이미지 업로드 시작...")

# � mainFrame 전환 제거 (이미 안에 있음)

# 네이버 글쓰기 창으로만 전환

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

self.logger.info("✅ 네이버 글쓰기 창으로 전환")

# 파일 다이얼로그 방지 설정 제거 (send_keys 방식 사용)

# 팝업 대기

self.logger.info("⏳ 이미지 업로드 옵션 팝업 대기 중...")

time.sleep(3)

# file input 찾기

self.logger.info("� 파일 input 요소 찾는 중...")

file_input_selectors = [

(By.CSS_SELECTOR, "input[type='file']"),

(By.XPATH, "//input[@type='file']"),

(By.CSS_SELECTOR, "input[accept*='image']"),

(By.CSS_SELECTOR, ".se-component-content input[type='file']"),

(By.CSS_SELECTOR, ".se-popup input[type='file']"),

]

file_input = None

for by, selector in file_input_selectors:

try:

file_input = WebDriverWait(self.driver, 5).until(

EC.presence_of_element_located((by, selector))

)

if file_input:

self.logger.info(f"✅ 파일 input 발견: {selector[:80]}...")

break

except TimeoutException:

continue

if not file_input:

self.logger.error("❌ 파일 input을 찾을 수 없습니다")

# 디버깅: 모든 input 출력

try:

all_inputs = self.driver.find_elements(By.TAG_NAME, "input")

self.logger.info(f"� 현재 페이지의 모든 input 요소 ({len(all_inputs)}개):")

for idx, inp in enumerate(all_inputs[:5]):

inp_type = inp.get_attribute('type')

inp_class = inp.get_attribute('class')

self.logger.info(f" {idx+1}. type={inp_type}, class={inp_class[:50] if inp_class else 'None'}")

except:

pass

return False

# 파일 경로 전달

all_file_paths = "\n".join(image_files)

self.logger.info("� 파일 경로를 input 요소에 전달 중...")

file_input.send_keys(all_file_paths)

# 업로드 대기

upload_wait_time = len(image_files) * 2

self.logger.info(f"⏳ {upload_wait_time}초 동안 업로드 완료 대기...")

time.sleep(upload_wait_time)

self.logger.info(f"✅ {len(image_files)}개 이미지 업로드 완료")

return True

except Exception as e:

self.logger.error(f"❌ 네이버 이미지 업로드 실패: {e}", exc_info=True)

return False


def _upload_single_image_to_naver(self, naver_write_window: str, image_file: str) -> bool:

"""네이버 블로그에 이미지 1개 업로드 (파일 다이얼로그 방지 강화)"""

try:

# 네이버 글쓰기 창으로 전환

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

# 팝업 대기

time.sleep(2)

# file input 찾기

file_input_selectors = [

(By.CSS_SELECTOR, "input[type='file']"),

(By.XPATH, "//input[@type='file']"),

(By.CSS_SELECTOR, "input[accept*='image']"),

]

file_input = None

for by, selector in file_input_selectors:

try:

file_input = WebDriverWait(self.driver, 5).until(

EC.presence_of_element_located((by, selector))

)

if file_input:

self.logger.info(f"✅ 파일 input 발견")

break

except TimeoutException:

continue

if not file_input:

self.logger.error("❌ 파일 input을 찾을 수 없습니다")

return False

filename = os.path.basename(image_file)

self.logger.info(f"� 파일 업로드 시도: {filename}")

# � 파일 다이얼로그 방지를 위한 추가 조치

try:

# 1. 파일 input을 숨김 처리하여 클릭 이벤트 방지

self.driver.execute_script("""

const fileInput = arguments[0];

fileInput.style.display = 'none';

fileInput.style.visibility = 'hidden';

fileInput.style.position = 'absolute';

fileInput.style.left = '-9999px';

""", file_input)

# 2. 파일 경로를 절대 경로로 변환

absolute_path = os.path.abspath(image_file)

# 3. send_keys()로 직접 업로드 (파일 다이얼로그 열리지 않음)

file_input.send_keys(absolute_path)

time.sleep(3)

self.logger.info("✅ send_keys() 방식으로 업로드 완료 (파일 다이얼로그 방지)")

return True

except Exception as upload_error:

self.logger.warning(f"⚠️ 첫 번째 업로드 시도 실패: {upload_error}")

# 4. 대안: JavaScript를 통한 파일 업로드

try:

self.logger.info("� JavaScript 방식으로 재시도...")

# JavaScript로 파일 input에 파일 경로 설정

self.driver.execute_script("""

const fileInput = arguments[0];

const filePath = arguments[1];

// 파일 input에 파일 경로 직접 설정

fileInput.value = filePath;

// change 이벤트 발생시켜 업로드 트리거

const changeEvent = new Event('change', { bubbles: true });

fileInput.dispatchEvent(changeEvent);

""", file_input, absolute_path)

time.sleep(3)

self.logger.info("✅ JavaScript 방식으로 업로드 완료")

return True

except Exception as js_error:

self.logger.error(f"❌ JavaScript 업로드도 실패: {js_error}")

return False

except Exception as e:

self.logger.error(f"❌ 이미지 업로드 실패: {e}", exc_info=True)

return False



def _close_file_dialog(self):

"""macOS 파일 선택 다이얼로그 닫기 (간소화된 버전)"""

try:

import pyautogui

self.logger.info("⌨️ ESC 키로 파일 선택 창 닫기...")

# ESC 키 3번 (확실하게)

for i in range(3):

pyautogui.press('escape')

time.sleep(0.2) # 0.3초 → 0.2초로 단축

self.logger.info("✅ 파일 선택 다이얼로그 닫기 완료")

except Exception as e:

self.logger.warning(f"⚠️ 파일 선택 창 닫기 실패: {e}")


def _get_first_image_caption_from_wp(self) -> str | None:

"""워드프레스에서 첫 번째 이미지의 figcaption 가져오기"""

try:

self.logger.info("� 워드프레스에서 첫 번째 이미지 출처 찾는 중...")

# 첫 번째 figcaption 찾기

figcaption_selectors = [

"figcaption",

".wp-caption-text",

".caption",

"p.caption"

]

for selector in figcaption_selectors:

try:

elements = self.driver.find_elements(By.CSS_SELECTOR, selector)

if elements:

caption = elements[0].text.strip()

if caption:

self.logger.info(f"✅ 출처 발견: {caption}")

return caption

except:

continue

self.logger.warning("⚠️ 워드프레스에서 출처를 찾지 못했습니다")

return None

except Exception as e:

self.logger.error(f"❌ 출처 추출 실패: {e}")

return None


def _input_image_source_to_naver(self, naver_write_window: str, caption: str) -> bool:

"""네이버 블로그에 이미지 출처 입력 (이미지 클릭/활성화 후 캡션 입력)"""

try:

self.logger.info("� 네이버 블로그 이미지 클릭/활성화 후 캡션 입력 시도 중...")

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

# mainFrame으로 전환 (최적화된 버전)

try:

# 현재 frame 상태 확인

current_frame = self.driver.execute_script("return window.frameElement;")

if current_frame is None:

# top level이면 mainFrame으로 전환 시도 (빠른 체크)

try:

WebDriverWait(self.driver, 2).until( # 5초 → 2초로 단축

EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame"))

)

self.logger.info("✅ mainFrame으로 전환 성공")

except Exception as frame_error:

self.logger.warning(f"⚠️ mainFrame 전환 실패: {frame_error}")

# mainFrame이 없을 수도 있으므로 계속 진행

pass

else:

self.logger.info("✅ 이미 iframe 내부에 있음")

except Exception as e:

self.logger.warning(f"⚠️ frame 상태 확인 실패: {e}")

# 오류 무시하고 계속 진행

time.sleep(1) # 3초 → 1초로 단축

# ===== Step 1: 업로드한 이미지 찾기 및 클릭 (활성화) - 이전 로직 유지 =====

self.logger.info("�️ Step 1: 업로드한 이미지 찾기...")

image_selectors = [

# 이미지 컴포넌트 내의 실제 이미지

(By.CSS_SELECTOR, ".se-component.se-image .se-image-resource"),

(By.CSS_SELECTOR, ".se-component.se-image img"),

(By.CSS_SELECTOR, "div.se-component.se-image"),

# SE- ID를 가진 이미지 컴포넌트

(By.CSS_SELECTOR, "div[id^='SE-'].se-component.se-image"),

]

uploaded_image = None

for by, selector in image_selectors:

try:

elements = self.driver.find_elements(by, selector)

if elements:

# 마지막에 업로드된 이미지 선택

uploaded_image = elements[-1]

self.logger.info(f"✅ 이미지 발견 (총 {len(elements)}개 중 마지막) - 셀렉터: {selector}")

break

except:

continue

if not uploaded_image:

self.logger.error("❌ 업로드된 이미지를 찾을 수 없습니다")

return False

# � 이미지가 화면에 보이도록 스크롤

self.logger.info("� 이미지로 스크롤...")

self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", uploaded_image)

time.sleep(1)

# � 여러 클릭 방법 시도

self.logger.info("�️ Step 1: 이미지 클릭 시도...")

click_success = False

# 방법 1: 일반 클릭

try:

uploaded_image.click()

time.sleep(2)

self.logger.info("✅ 일반 클릭 성공")

click_success = True

except Exception as e:

self.logger.warning(f"⚠️ 일반 클릭 실패: {e}")

# 방법 2: ActionChains 클릭

if not click_success:

try:

ActionChains(self.driver).move_to_element(uploaded_image).click().perform()

time.sleep(2)

self.logger.info("✅ ActionChains 클릭 성공")

click_success = True

except Exception as e:

self.logger.warning(f"⚠️ ActionChains 클릭 실패: {e}")

# 방법 3: JavaScript 클릭

if not click_success:

try:

self.driver.execute_script("arguments[0].click();", uploaded_image)

time.sleep(2)

self.logger.info("✅ JavaScript 클릭 성공")

click_success = True

except Exception as e:

self.logger.warning(f"⚠️ JavaScript 클릭 실패: {e}")

# 방법 4: 부모 요소 클릭

if not click_success:

try:

# 부모 요소를 찾아 클릭 (가끔 실제 이미지가 아닌 컴포넌트 div를 클릭해야 할 때가 있음)

parent = uploaded_image.find_element(By.XPATH, "./ancestor::div[contains(@class, 'se-component-content')]")

parent.click()

time.sleep(2)

self.logger.info("✅ 부모 요소 클릭 성공")

click_success = True

except Exception as e:

self.logger.warning(f"⚠️ 부모 요소 클릭 실패: {e}")

# � 성공했다고 가정하고 이후 로직으로 넘어갑니다.

# 만약 Step 1이 실패하면 여기서 return False 됩니다.

if not click_success:

self.logger.error("❌ 이미지 클릭/활성화 실패로 캡션 입력 불가.")

return False


self.logger.info("✅ 이미지 클릭 완료 - 캡션 입력 필드 활성화 대기 중...")

time.sleep(3) # 활성화 대기 시간

# ===== Step 2: 사진 설명란 찾기 및 두 번 클릭하여 편집 모드 활성화 =====

self.logger.info("� Step 2: 사진 설명란 찾기 및 두 번 클릭...")


caption_input = None

max_attempts = 5

for attempt in range(max_attempts):

self.logger.info(f" - 시도 {attempt + 1}/{max_attempts}: 캡션 요소 찾기...")

try:

# 1단계: se-caption div 내의 p 태그 찾기

# 일반적으로 캡션 입력 필드는 se-caption 내의 p.se-text-paragraph 입니다.

p_elements = self.driver.find_elements(By.CSS_SELECTOR, "div.se-caption p.se-text-paragraph")

if p_elements:

# 마지막에 활성화된 이미지의 캡션 필드 선택 (네이버 에디터 특성상 마지막 컴포넌트가 활성화됨)

caption_input = p_elements[-1]

self.logger.info("✅ 사진 설명란(p 태그) 발견")

break

else:

# p 태그가 없으면 se-caption div를 클릭해서 생성 시도

caption_divs = self.driver.find_elements(By.CSS_SELECTOR, "div.se-caption")

if caption_divs:

self.logger.info(" - p 태그가 없어서 se-caption 영역 클릭하여 생성 시도...")

caption_divs[-1].click()

time.sleep(1)

# 다시 p 태그 검색

p_elements_retry = self.driver.find_elements(By.CSS_SELECTOR, "div.se-caption p.se-text-paragraph")

if p_elements_retry:

caption_input = p_elements_retry[-1]

self.logger.info("✅ 사진 설명란(p 태그) 생성됨")

break

except Exception as e:

self.logger.warning(f" - 시도 {attempt + 1} 실패: {e}")

time.sleep(1)

continue

if not caption_input:

self.logger.error("❌ 모든 시도 후에도 캡션 요소를 찾을 수 없습니다")

return False


# 2단계: 캡션 입력 필드를 두 번 클릭하여 편집 모드 활성화

self.logger.info("�️ Step 2-1: 캡션 입력 필드 두 번 클릭 시도...")

try:

# 캡션 입력 필드를 두 번 클릭 (편집 모드 진입)

ActionChains(self.driver).double_click(caption_input).perform()

time.sleep(1)

self.logger.info("✅ 두 번 클릭 성공. 편집 모드 활성화됨.")

except Exception as e:

self.logger.error(f"❌ 캡션 필드 두 번 클릭 실패: {e}")

return False


# ===== Step 2-2: 실제 텍스트 입력 대상인 <span> 요소 획득 =====

try:

# p 태그 내부에 있는 span 태그 (실제 텍스트가 입력되는 곳)를 찾습니다.

actual_text_span = caption_input.find_element(By.CSS_SELECTOR, "span")

self.logger.info("✅ 캡션 입력 대상 <span> 요소 획득 성공.")

except NoSuchElementException:

self.logger.warning("⚠️ p 태그 내부에 <span> 요소를 찾을 수 없습니다. p 태그 자체를 입력 대상으로 사용합니다.")

actual_text_span = caption_input

# **********************************************

# 이후 모든 send_keys 및 ActionChains의 대상은

# 'caption_input' 대신 'actual_text_span'으로 변경되어야 합니다.

# **********************************************


# ===== Step 3: 클립보드 복사 후 붙여넣기 방식으로 출처 입력 (가장 안정적) =====

self.logger.info(f"⌨️ Step 3: 클립보드 붙여넣기로 출처 입력 중... ('{caption}')")

# 1. 출처 텍스트를 클립보드에 복사

try:

import pyperclip

pyperclip.copy(caption)

time.sleep(0.5)

self.logger.info(" - 클립보드 복사 완료.")

except Exception as e:

self.logger.error(f"❌ pyperclip 복사 실패: {e}")

return False


# 2. 포커스 확보 및 기존 내용 제거 (붙여넣기 전에 필수)

modifier = Keys.COMMAND if sys.platform == 'darwin' else Keys.CONTROL

try:

# 캡션 필드에 포커스를 다시 주고 (actual_text_span을 클릭)

ActionChains(self.driver).move_to_element(actual_text_span).click().perform()

time.sleep(0.5)

# 전체 선택 (Ctrl/Cmd + A) -> 삭제

ActionChains(self.driver)\

.key_down(modifier).send_keys('a').key_up(modifier)\

.pause(0.3)\

.send_keys(Keys.DELETE)\

.pause(0.5)\

.perform()

self.logger.info(" - 기존 내용 제거 완료.")

except Exception as e:

self.logger.warning(f" - ⚠️ 기존 내용 제거 실패 (계속 진행): {e}")


# 3. 붙여넣기 (Ctrl/Cmd + V)

self.logger.info(" - 클립보드 내용 붙여넣기 (Ctrl/Cmd + V)...")

ActionChains(self.driver)\

.key_down(modifier).send_keys('v').key_up(modifier)\

.pause(1.0)\

.perform()

time.sleep(1)

# 4. 포커스 이동 (변경사항 확정)

try:

ActionChains(self.driver).send_keys(Keys.TAB).perform()

time.sleep(0.5)

except:

try:

self.driver.find_element(By.CSS_SELECTOR, "div.se-module.se-module-text").click()

time.sleep(0.5)

except:

pass

# 5. 입력 확인 로직 (최종 검증)

input_value = self.driver.execute_script("return arguments[0].textContent;", actual_text_span)

if input_value and input_value.replace(' ', '') in caption.replace(' ', '') and len(input_value) > len(caption) * 0.8:

self.logger.info(f"✅ 입력 검증 성공: '{input_value[:50]}...'")

else:

self.logger.warning(f"⚠️ 입력 검증 실패. 기대값: '{caption[:30]}...', 실제 입력값: '{input_value[:30]}...'")

# 6. ✨ Enter 키를 눌러서 커서를 아래로 이동 (본문 작성 준비)

self.logger.info("⏎ Enter 키를 눌러 캡션 입력을 완료하고 본문 영역으로 이동...")

try:

# 본문 영역으로 포커스 이동 후 Enter

ActionChains(self.driver)\

.send_keys(Keys.ESCAPE)\

.pause(0.5)\

.send_keys(Keys.ENTER)\

.pause(0.5)\

.perform()

self.logger.info("✅ Enter 키 입력 완료 - 본문 작성 가능한 상태")

except Exception as e:

self.logger.warning(f"⚠️ Enter 키 입력 중 오류 (무시하고 계속): {e}")

self.logger.info("� 캡션(출처) 입력 및 Enter 키 입력까지 모두 완료!")

return True

except Exception as e:

self.logger.error(f"❌ 캡션 입력 전체 시퀀스 실패: {e}", exc_info=True)

return False






def _activate_caption_javascript(self, caption_input) -> bool:

"""JavaScript로 강제 포커스"""

try:

self.driver.execute_script("""

var element = arguments[0];

// 1. 스크롤해서 보이게 하기

element.scrollIntoView({block: 'center'});

// 2. 강제 포커스

element.focus();

// 3. 클릭 이벤트 트리거

var clickEvent = new MouseEvent('click', {

bubbles: true,

cancelable: true,

view: window

});

element.dispatchEvent(clickEvent);

// 4. contenteditable 활성화

element.setAttribute('contenteditable', 'true');

return true;

""", caption_input)

time.sleep(1)

return True

except:

return False


def _activate_caption_double_click(self, caption_input) -> bool:

"""더블 클릭으로 활성화"""

try:

ActionChains(self.driver).double_click(caption_input).perform()

time.sleep(1)

return True

except:

return False


def _activate_caption_actionchains(self, caption_input) -> bool:

"""ActionChains로 클릭"""

try:

ActionChains(self.driver).move_to_element(caption_input).click().perform()

time.sleep(1)

return True

except:

return False


def _activate_caption_normal(self, caption_input) -> bool:

"""일반 클릭"""

try:

caption_input.click()

time.sleep(1)

return True

except:

return False


def _activate_caption_events(self, caption_input) -> bool:

"""JavaScript 이벤트 트리거"""

try:

self.driver.execute_script("""

var element = arguments[0];

// 여러 이벤트 트리거

var events = ['mousedown', 'mouseup', 'click', 'focus', 'focusin'];

events.forEach(function(eventType) {

var event = new Event(eventType, { bubbles: true, cancelable: true });

element.dispatchEvent(event);

});

// contenteditable 강제 설정

element.setAttribute('contenteditable', 'true');

element.setAttribute('tabindex', '0');

return true;

""", caption_input)

time.sleep(1)

return True

except:

return False


def _type_naver_text_to_body(self, naver_write_window: str, record_id: str) -> bool:

"""

Media CMS의 naver_text를 가져와서 첫 번째 소제목 전까지 타이핑 + 소제목도 입력

[DEPRECATED] 이 메서드는 더 이상 사용되지 않습니다. _insert_full_content_with_images_from_naver_cms를 사용하세요.

"""

try:

self.logger.error("❌ 이 메서드는 더 이상 사용되지 않습니다. Media CMS는 제거되었습니다.")

return False

# Legacy code - Media CMS 사용 부분 제거됨

# naver_text_html = None

# 2. 첫 번째 19px 소제목 추출

import re

first_subtitle_pattern = r'<p><b><span style="font-size: 19px;">([^<]+)</span></b></p>'

subtitle_match = re.search(first_subtitle_pattern, naver_text_html)

first_subtitle = ""

if subtitle_match:

first_subtitle = subtitle_match.group(1)

self.logger.info(f"� 첫 번째 소제목 발견: {first_subtitle}")

# 3. 첫 번째 19px 소제목 이전까지의 내용 추출

first_subtitle_pattern_split = r'<p><b><span style="font-size: 19px;">'

parts = naver_text_html.split(first_subtitle_pattern_split)

if len(parts) < 2:

text_before_subtitle = naver_text_html

self.logger.warning("⚠️ 소제목을 찾을 수 없어 전체 텍스트 사용")

else:

text_before_subtitle = parts[0]

self.logger.info("✅ 첫 번째 소제목 이전 텍스트 추출 완료")

# 4. 네이버 글쓰기 창으로 전환

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

# mainFrame 확인

try:

WebDriverWait(self.driver, 5).until(

EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame"))

)

except:

pass # 이미 mainFrame 안에 있을 수 있음

# 5. HTML 구조를 파싱하면서 타이핑

self.logger.info("⌨️ HTML 구조에 따라 텍스트 타이핑 시작...")

# HTML을 요소별로 분리 (텍스트와 태그 구분)

# <br>, </p><p>, </p> 등을 기준으로 분리

html_parts = re.split(r'(<br>|</p><p>|</p>|<p>|<[^>]+>)', text_before_subtitle)

for part in html_parts:

if not part:

continue

if part == '<br>':

# <br> 태그는 Enter 두 번

self.logger.debug("↵ <br> 태그: Enter 두 번")

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.1)

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.1)

elif part == '</p><p>' or part == '</p>':

# 단락 구분도 Enter 두 번

if part == '</p><p>':

self.logger.debug("↵ </p><p> 태그: Enter 두 번")

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.1)

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.1)

# </p>만 있으면 무시 (문장 끝)

elif part.startswith('<') and part.endswith('>'):

# 다른 HTML 태그는 무시

self.logger.debug(f"�️ HTML 태그 무시: {part}")

continue

else:

# 일반 텍스트는 문자별로 타이핑

clean_text = re.sub(r'<[^>]+>', '', part) # 혹시 남은 태그 제거

if clean_text.strip():

self.logger.debug(f"� 텍스트 타이핑: {clean_text[:30]}...")

for char in clean_text:

ActionChains(self.driver).send_keys(char).perform()

time.sleep(0.03) # 각 문자 사이 0.03초 지연

self.logger.info("✅ 첫 번째 소제목 전까지 본문 타이핑 완료")

# 6. 마지막에 Enter 한 번 (다음 이미지를 위한 공간)

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.2)

# 7. 첫 번째 소제목 입력 (있을 경우)

if first_subtitle:

self.logger.info("� 소제목 입력 시작...")

if not self._type_and_format_subtitle(naver_write_window, first_subtitle):

self.logger.warning("⚠️ 소제목 포맷팅 실패")

else:

self.logger.info("✅ 소제목 입력 완료")

self.logger.info("✅ 본문 입력 후 줄바꿈 완료 (다음 이미지 삽입 가능)")

return True

except Exception as e:

self.logger.error(f"❌ 본문 텍스트 입력 실패: {e}", exc_info=True)

return False


def _type_and_format_subtitle(self, naver_write_window: str, subtitle_text: str) -> bool:

"""

소제목을 타이핑하고 포맷팅 (19px 굵게)

"""

try:

self.logger.info(f"� 소제목 입력 및 포맷팅 시작: {subtitle_text[:30]}...")

# 1. 네이버 글쓰기 창 확인

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

# mainFrame 확인 (최적화된 버전)

try:

# 현재 frame 상태 확인

current_frame = self.driver.execute_script("return window.frameElement;")

if current_frame is None:

# top level이면 mainFrame으로 전환 시도 (빠른 체크)

try:

WebDriverWait(self.driver, 2).until( # 5초 → 2초로 단축

EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame"))

)

self.logger.info("✅ mainFrame으로 전환 성공")

except Exception as frame_error:

self.logger.warning(f"⚠️ mainFrame 전환 실패: {frame_error}")

# mainFrame이 없을 수도 있으므로 계속 진행

pass

else:

self.logger.info("✅ 이미 iframe 내부에 있음")

except Exception as e:

self.logger.warning(f"⚠️ frame 상태 확인 실패: {e}")

# 오류 무시하고 계속 진행

# 2. 소제목 텍스트 타이핑 (최적화된 버전)

self.logger.info("⌨️ 소제목 타이핑 중...")

# 한 번에 전체 텍스트 입력 (문자별 타이핑 제거)

ActionChains(self.driver).send_keys(subtitle_text).perform()

time.sleep(0.3) # 0.5초 → 0.3초로 단축

# 4. 텍스트 포맷 버튼 클릭 (최적화된 버전)

self.logger.info("� 텍스트 포맷 메뉴 열기...")

# 가장 일반적인 셀렉터부터 빠르게 시도

format_button_selectors = [

(By.CSS_SELECTOR, "li.se-toolbar-item.se-toolbar-item-text-format > div > button"),

(By.CSS_SELECTOR, ".se-toolbar-item-text-format button"),

(By.XPATH, "//li[contains(@class, 'se-toolbar-item-text-format')]//button"),

]

format_button_found = False

for by, selector in format_button_selectors:

try:

format_button = WebDriverWait(self.driver, 2).until( # 3초 → 2초로 단축

EC.element_to_be_clickable((by, selector))

)

format_button.click()

time.sleep(0.5) # 1초 → 0.5초로 단축

format_button_found = True

self.logger.info("✅ 텍스트 포맷 메뉴 열기 성공")

break

except:

continue

if not format_button_found:

self.logger.error("❌ 텍스트 포맷 버튼을 찾을 수 없습니다")

return False

# 5. 소제목 옵션 선택 (최적화된 버전)

self.logger.info("� 소제목 포맷 선택...")

# 가장 일반적인 셀렉터부터 빠르게 시도

subtitle_option_selectors = [

(By.XPATH, "//span[text()='소제목']"),

(By.XPATH, "//button[contains(text(), '소제목')]"),

(By.CSS_SELECTOR, "button[data-format='subtitle']"),

]

subtitle_selected = False

for by, selector in subtitle_option_selectors:

try:

subtitle_option = WebDriverWait(self.driver, 2).until( # 3초 → 2초로 단축

EC.element_to_be_clickable((by, selector))

)

subtitle_option.click()

time.sleep(0.3) # 0.5초 → 0.3초로 단축

subtitle_selected = True

self.logger.info("✅ 소제목 포맷 적용 완료")

break

except:

continue

if not subtitle_selected:

self.logger.warning("⚠️ 소제목 옵션을 찾을 수 없어 기본 텍스트로 유지")

# 6. 커서를 소제목 끝으로 이동하고 Enter

ActionChains(self.driver).send_keys(Keys.END).perform()

time.sleep(0.2)

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.2)

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

self.logger.info("✅ 소제목 입력 및 포맷팅 완료")

return True

except Exception as e:

self.logger.error(f"❌ 소제목 입력 실패: {e}", exc_info=True)

return False


def _insert_full_content_with_images(self, naver_write_window: str, record_id: str, wp_url: str) -> bool:

"""

전체 콘텐츠를 삽입 (첫 번째 이미지부터 시작)

[DEPRECATED] 이 메서드는 더 이상 사용되지 않습니다. _insert_full_content_with_images_from_naver_cms를 사용하세요.

"""

try:

self.logger.error("❌ 이 메서드는 더 이상 사용되지 않습니다. Media CMS는 제거되었습니다.")

return False

# Legacy code - Media CMS 사용 부분 제거됨

# naver_text_html = None

# 2. 워드프레스에서 모든 이미지 다운로드

self.logger.info("�️ 워드프레스에서 모든 이미지 다운로드...")

if not self.switch_to_tab('content'):

return False

self.driver.get(wp_url)

WebDriverWait(self.driver, 15).until(

lambda d: d.execute_script('return document.readyState') == 'complete'

)

time.sleep(3)

image_data = self._extract_wp_images_and_captions()

downloaded_images = []

for img_info in image_data:

image_url = img_info.get('image_url')

caption = img_info.get('caption', '')

if image_url:

file_path = self._download_image_to_temp(image_url)

if file_path:

downloaded_images.append({

'path': file_path,

'caption': caption

})

self.logger.info(f"✅ 총 {len(downloaded_images)}개 이미지 다운로드 완료")

# 3. naver_text를 구조적으로 파싱

import re

# 모든 콘텐츠를 순서대로 파싱 (문장, 소제목 구분)

content_blocks = []

# HTML을 블록 단위로 분리

blocks = re.findall(r'<p>.*?</p>', naver_text_html)

for block in blocks:

# 소제목인지 확인 (19px)

if 'font-size: 19px' in block:

# 소제목

subtitle_text = re.sub(r'<[^>]+>', '', block).strip()

if subtitle_text:

content_blocks.append({

'type': 'subtitle',

'text': subtitle_text

})

else:

# 일반 문장

sentence_text = re.sub(r'<[^>]+>', '', block).strip()

if sentence_text:

content_blocks.append({

'type': 'sentence',

'text': sentence_text

})

self.logger.info(f"� 총 {len(content_blocks)}개 블록 파싱 완료")

# 4. 네이버 글쓰기 창으로 전환

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

try:

WebDriverWait(self.driver, 5).until(

EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame"))

)

except:

pass

# ✨ 5. 첫 번째 이미지를 콘텐츠 시작 전에 삽입

if downloaded_images:

self.logger.info("�️ 첫 번째 이미지 삽입 (콘텐츠 시작 전)...")

if self._click_naver_image_upload_button(naver_write_window):

first_img = downloaded_images[0]

if self._upload_single_image_to_naver(naver_write_window, first_img['path']):

time.sleep(2)

self._close_file_dialog()

# 첫 번째 이미지 캡션 입력

if first_img['caption']:

self._input_image_source_to_naver(naver_write_window, first_img['caption'])

else:

# 캡션 없으면 Enter만

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.2)

self.logger.info("✅ 첫 번째 이미지 삽입 완료")

else:

self.logger.warning("⚠️ 첫 번째 이미지 업로드 실패")

else:

self.logger.warning("⚠️ 첫 번째 이미지 버튼 클릭 실패")

# 6. 콘텐츠 삽입 (5줄마다 이미지)

line_counter = 0

image_index = 1 # ✅ 1부터 시작 (첫 번째 이미지는 이미 삽입됨)

for i, block in enumerate(content_blocks):

line_counter += 1

# 텍스트 블록 먼저 삽입

if block['type'] == 'subtitle':

# 소제목 타이핑 및 포맷팅

self.logger.info(f"� 소제목 {line_counter}: {block['text'][:30]}...")

self._type_and_format_subtitle(naver_write_window, block['text'])

else: # sentence

# 일반 문장 타이핑

self.logger.info(f"� 문장 {line_counter}: {block['text'][:30]}...")

for char in block['text']:

ActionChains(self.driver).send_keys(char).perform()

time.sleep(0.03)

# 문장 끝에 Enter 두 번

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.1)

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.1)

# 5줄마다 이미지 삽입 (5, 10, 15...)

if line_counter % 5 == 0 and image_index < len(downloaded_images):

self.logger.info(f"�️ {line_counter}줄 후 이미지 {image_index + 1}번째 삽입...")

# 이미지 버튼 클릭

if self._click_naver_image_upload_button(naver_write_window):

img_info = downloaded_images[image_index]

# 이미지 업로드

if self._upload_single_image_to_naver(naver_write_window, img_info['path']):

time.sleep(2)

self._close_file_dialog()

# 캡션 입력

if img_info['caption']:

self._input_image_source_to_naver(naver_write_window, img_info['caption'])

else:

# 캡션 없으면 Enter만

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.2)

image_index += 1

self.logger.info(f"✅ 이미지 {image_index}번째 삽입 완료")

elif line_counter % 5 == 0 and image_index >= len(downloaded_images):

# ✅ 이미지가 더 이상 없으면 로그만 남기고 계속 진행

self.logger.info(f"ℹ️ {line_counter}줄 시점: 남은 이미지가 없어 텍스트만 계속 입력")

# 6. 남은 이미지 처리 부분 제거 (중복 방지)

if image_index < len(downloaded_images):

self.logger.info(f"ℹ️ 사용하지 않은 이미지 {len(downloaded_images) - image_index}개 있음 (중복 방지를 위해 사용 안 함)")

# 7. 임시 파일 정리

self._clean_temp_uploads()

self.logger.info(f"✅ 전체 콘텐츠 삽입 완료! (사용된 이미지: {image_index}/{len(downloaded_images)}개)")

return True

except Exception as e:

self.logger.error(f"❌ 전체 콘텐츠 삽입 실패: {e}", exc_info=True)

return False


def _insert_full_content_with_images_from_naver_cms(

self,

naver_write_window: str,

naver_cms_info: dict,

wp_url: str

) -> bool:

"""

Naver CMS 데이터를 사용하여 전체 콘텐츠 삽입

(기존 _insert_full_content_with_images 메서드 수정)

"""

try:

self.logger.info("� Naver CMS 데이터로 전체 콘텐츠 삽입 시작...")

# 1. naver_text 가져오기 (이미 naver_cms_info에 있음)

naver_text_html = naver_cms_info.get('naver_text', '')

if not naver_text_html:

self.logger.error("❌ naver_text가 비어있습니다")

return False

# 2. 워드프레스에서 모든 이미지 다운로드

self.logger.info("�️ 워드프레스에서 모든 이미지 다운로드...")

if not self.switch_to_tab('content'):

return False

self.driver.get(wp_url)

WebDriverWait(self.driver, 15).until(

lambda d: d.execute_script('return document.readyState') == 'complete'

)

time.sleep(3)

image_data = self._extract_wp_images_and_captions()

downloaded_images = []

for img_info in image_data:

image_url = img_info.get('image_url')

caption = img_info.get('caption', '')

if image_url:

file_path = self._download_image_to_temp(image_url)

if file_path:

downloaded_images.append({

'path': file_path,

'caption': caption

})

self.logger.info(f"✅ 총 {len(downloaded_images)}개 이미지 다운로드 완료")

# 3. naver_text를 구조적으로 파싱

import re

# 모든 콘텐츠를 순서대로 파싱 (문장, 소제목 구분)

content_blocks = []

# HTML을 블록 단위로 분리

blocks = re.findall(r'<p>.*?</p>', naver_text_html)

for block in blocks:

# 소제목인지 확인 (19px)

if 'font-size: 19px' in block:

# 소제목

subtitle_text = re.sub(r'<[^>]+>', '', block).strip()

if subtitle_text:

content_blocks.append({

'type': 'subtitle',

'text': subtitle_text

})

else:

# 일반 문장

sentence_text = re.sub(r'<[^>]+>', '', block).strip()

if sentence_text:

content_blocks.append({

'type': 'sentence',

'text': sentence_text

})

self.logger.info(f"� 총 {len(content_blocks)}개 블록 파싱 완료")

# 4. 네이버 글쓰기 창으로 전환

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)

try:

WebDriverWait(self.driver, 5).until(

EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame"))

)

except:

pass

# ✨ 5. 첫 번째 이미지를 콘텐츠 시작 전에 삽입

if downloaded_images:

self.logger.info("�️ 첫 번째 이미지 삽입 (콘텐츠 시작 전)...")

if self._click_naver_image_upload_button(naver_write_window):

first_img = downloaded_images[0]

if self._upload_single_image_to_naver(naver_write_window, first_img['path']):

time.sleep(2)

self._close_file_dialog()

# 첫 번째 이미지 캡션 입력

if first_img['caption']:

self._input_image_source_to_naver(naver_write_window, first_img['caption'])

else:

# 캡션 없으면 Enter만

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.2)

self.logger.info("✅ 첫 번째 이미지 삽입 완료")

else:

self.logger.warning("⚠️ 첫 번째 이미지 업로드 실패")

else:

self.logger.warning("⚠️ 첫 번째 이미지 버튼 클릭 실패")

# 6. 콘텐츠 삽입 (5줄마다 이미지)

line_counter = 0

image_index = 1 # ✅ 1부터 시작 (첫 번째 이미지는 이미 삽입됨)

for i, block in enumerate(content_blocks):

line_counter += 1

# 텍스트 블록 먼저 삽입

if block['type'] == 'subtitle':

# 소제목 타이핑 및 포맷팅

self.logger.info(f"� 소제목 {line_counter}: {block['text'][:30]}...")

self._type_and_format_subtitle(naver_write_window, block['text'])

else: # sentence

# 일반 문장 타이핑

self.logger.info(f"� 문장 {line_counter}: {block['text'][:30]}...")

for char in block['text']:

ActionChains(self.driver).send_keys(char).perform()

time.sleep(0.03)

# 문장 끝에 Enter 두 번

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.1)

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.1)

# 5줄마다 이미지 삽입 (5, 10, 15...)

if line_counter % 5 == 0 and image_index < len(downloaded_images):

self.logger.info(f"�️ {line_counter}줄 후 이미지 {image_index + 1}번째 삽입...")

# 이미지 버튼 클릭

if self._click_naver_image_upload_button(naver_write_window):

img_info = downloaded_images[image_index]

# 이미지 업로드

if self._upload_single_image_to_naver(naver_write_window, img_info['path']):

time.sleep(2)

self._close_file_dialog()

# 캡션 입력

if img_info['caption']:

self._input_image_source_to_naver(naver_write_window, img_info['caption'])

else:

# 캡션 없으면 Enter만

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.2)

image_index += 1

self.logger.info(f"✅ 이미지 {image_index}번째 삽입 완료")

elif line_counter % 5 == 0 and image_index >= len(downloaded_images):

# ✅ 이미지가 더 이상 없으면 로그만 남기고 계속 진행

self.logger.info(f"ℹ️ {line_counter}줄 시점: 남은 이미지가 없어 텍스트만 계속 입력")

# 6. 남은 이미지 처리 부분 제거 (중복 방지)

if image_index < len(downloaded_images):

self.logger.info(f"ℹ️ 사용하지 않은 이미지 {len(downloaded_images) - image_index}개 있음 (중복 방지를 위해 사용 안 함)")

# 7. 임시 파일 정리

self._clean_temp_uploads()

self.logger.info(f"✅ 전체 콘텐츠 삽입 완료! (사용된 이미지: {image_index}/{len(downloaded_images)}개)")

return True

except Exception as e:

self.logger.error(f"❌ 전체 콘텐츠 삽입 실패: {e}", exc_info=True)

return False


def _add_box_thumbnail_to_naver(self, naver_write_window: str, record_index: int = 0, current_wp_url: str = None) -> bool:

"""

네이버 블로그에 박스형 썸네일(링크 첨부) 1개 추가 테스트

순서: 링크 버튼 클릭 → <URL을 입력하세요> 활성화 → URL 붙여넣기 → enter → 대기시간 1초 → 확인

"""

try:

self.logger.info("� 네이버 블로그 박스형 썸네일 추가 시작...")


# 1. 최신 레코드 가져오기 (7개 조회 후 현재 URL 제외)

latest_records = self.airtable_manager.table.all(max_records=7, sort=["-created_time"])

# 현재 발행 중인 글 제외

filtered_records = []

for record in latest_records:

wp_link = record.get('fields', {}).get('wp_link')

if wp_link and wp_link != current_wp_url:

filtered_records.append(record)

if len(filtered_records) >= 6:

break

if not filtered_records or len(filtered_records) <= record_index:

self.logger.error(f"❌ Airtable에서 레코드를 찾을 수 없습니다 (인덱스: {record_index})")

return False


url = filtered_records[record_index].get('fields', {}).get('wp_link')

if not url:

self.logger.error("❌ wp_link가 없습니다")

return False


self.logger.info(f"� URL: {url}")


# 2. 네이버 글쓰기 창으로 전환

if self.driver.current_window_handle != naver_write_window:

self.driver.switch_to.window(naver_write_window)


# mainFrame 확인 (빠른 체크)

try:

# 현재 frame 확인

current_frame = self.driver.execute_script("return window.frameElement;")

if current_frame is None:

# top level이면 mainFrame으로 전환

WebDriverWait(self.driver, 2).until( # 5초 → 2초로 단축

EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame"))

)

self.logger.info("✅ mainFrame으로 전환")

else:

self.logger.info("✅ 이미 iframe 내부에 있음")

except Exception as frame_error:

self.logger.warning(f"⚠️ frame 전환 오류 (계속 진행): {frame_error}")

# 오류 무시하고 계속


# 3. 링크 버튼 클릭 (제공된 셀렉터 사용)

self.logger.info("� 링크 버튼 찾는 중...")


link_button_selectors = [

# 일반적인 셀렉터 (빠른 시도)

(By.CSS_SELECTOR, "li.se-toolbar-item.se-toolbar-item-oglink > button"),

(By.XPATH, "//li[contains(@class, 'se-toolbar-item-oglink')]//button"),

# 제공된 셀렉터 (백업)

(By.CSS_SELECTOR, "#SE-5c17123d-4b8e-41c2-adcf-c55f49a091a2 > div.se-wrap.se-dnd-wrap > div > header > div.se-header-inbox.se-l-document-toolbar > ul > li.se-toolbar-item.se-toolbar-item-oglink > button"),

(By.XPATH, "//*[@id='SE-5c17123d-4b8e-41c2-adcf-c55f49a091a2']/div[1]/div/header/div[1]/ul/li[7]/button"),

]


link_button_found = False

for by, selector in link_button_selectors:

try:

link_button = WebDriverWait(self.driver, 2).until( # 5초 → 2초로 단축

EC.element_to_be_clickable((by, selector))

)


# 스크롤하여 버튼이 보이도록

self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", link_button)

time.sleep(0.5)


# 버튼 클릭

try:

link_button.click()

except:

self.driver.execute_script("arguments[0].click();", link_button)


time.sleep(2)

link_button_found = True

self.logger.info("✅ 링크 버튼 클릭 성공")

break


except TimeoutException:

continue


if not link_button_found:

self.logger.error("❌ 링크 버튼을 찾을 수 없습니다")

return False


# 4. URL 직접 입력 (팝업창이 뜨면서 이미 커서가 있는 상태)

self.logger.info("� URL 직접 입력...")

# 기존 내용 완전히 지우기

self.logger.info("�️ 기존 URL 내용 지우기...")

modifier = Keys.COMMAND if sys.platform == 'darwin' else Keys.CONTROL

# 전체 선택 후 삭제

ActionChains(self.driver).key_down(modifier).send_keys('a').key_up(modifier).perform()

time.sleep(0.2)

ActionChains(self.driver).send_keys(Keys.DELETE).perform()

time.sleep(0.5)

# URL을 클립보드에 복사

pyperclip.copy(url)

time.sleep(0.5)

# 방법 1: 키보드 단축키로 붙여넣기

try:

ActionChains(self.driver).key_down(modifier).send_keys('v').key_up(modifier).perform()

time.sleep(1)

self.logger.info(f"✅ URL 입력 완료: {url[:50]}...")

except Exception as e:

self.logger.warning(f"⚠️ 붙여넣기 실패, 직접 타이핑 시도: {e}")

# 방법 2: 직접 타이핑

for char in url:

ActionChains(self.driver).send_keys(char).perform()

time.sleep(0.05)

time.sleep(1)

self.logger.info(f"✅ URL 직접 입력 완료: {url[:50]}...")


# 5. Enter 키 입력

self.logger.info("⌨️ Enter 키 입력...")

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(5) # URL 입력 후 대기시간 5초


# 6. 확인 버튼 클릭 (제공된 셀렉터 사용)

self.logger.info("✅ 확인 버튼 클릭...")


confirm_button_selectors = [

# 일반적인 셀렉터 (빠른 시도)

(By.CSS_SELECTOR, "button.se-popup-button-confirm"),

(By.XPATH, "//button[contains(text(), '확인')]"),

# 제공된 셀렉터 (백업)

(By.CSS_SELECTOR, "#SE-5db7e9c3-9f0d-41ba-a7a3-dffaef0e2a6a > div.se-wrap.se-dnd-wrap > div > div.se-popup.__se-sentry.se-popup-oglink > div.se-popup-container.__se-pop-layer > div.se-popup-button-container > button"),

(By.XPATH, "//*[@id='SE-5db7e9c3-9f0d-41ba-a7a3-dffaef0e2a6a']/div[1]/div/div[4]/div[2]/div[3]/button"),

]


confirm_button_found = False

for by, selector in confirm_button_selectors:

try:

confirm_button = WebDriverWait(self.driver, 2).until( # 5초 → 2초로 단축

EC.element_to_be_clickable((by, selector))

)

confirm_button.click()

time.sleep(1) # 확인 버튼 클릭 후 대기시간 1초

confirm_button_found = True

self.logger.info("✅ 확인 버튼 클릭 성공")

break


except TimeoutException:

continue


if not confirm_button_found:

self.logger.warning("⚠️ 확인 버튼을 찾을 수 없어 Enter 키로 시도...")

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(1) # Enter 키 후 대기시간 1초


# 7. 다음 썸네일을 위해 Enter 추가

ActionChains(self.driver).send_keys(Keys.ENTER).perform()

time.sleep(0.5)


self.logger.info("✅ 박스형 썸네일 삽입 완료")

return True


except Exception as e:

self.logger.error(f"❌ 박스형 썸네일 추가 실패: {e}", exc_info=True)

return False

keyword
작가의 이전글관세 낮췄다더니… 현대차·기아 8.4조 날릴 위기