brunch

“여행의 묘미 싹 사라진다?”…’초유의 사태’에 업계

by 위드카 뉴스

신라·신세계면세점 철수 위기
면세점은 이미 적자 늪에
이미 예견되었던 상황

IncheonAirport-DutyFree-Exit-Crisis-1-1024x576.jpg

인천공항 면세점 철수 위기 / 출처 : 연합뉴스


여행을 앞둔 시민들에게 충격적인 소식이 전해졌다.



인천국제공항의 대표 면세점들이 잇따라 문을 닫을 위기에 놓인 것이다.


출국객 수는 빠르게 늘고 있지만, 정작 면세점들은 적자에 허덕이며 운영 포기까지 고민하는 상황이다.


감정평가 결과, 임대료 40% 인하 권고


IncheonAirport-DutyFree-Exit-Crisis-2-1024x584.jpg

인천공항 면세점 철수 위기 / 출처 : 연합뉴스


신라면세점과 신세계면세점은 인천공항 제1·2여객터미널의 화장품·향수·주류·담배 구역 임대료를 40% 인하해 달라며 법원에 조정을 신청했다.


이에 따라 인천지방법원이 삼일회계법인에 감정을 의뢰했고, 그 결과 면세점 재입찰 시 적정 임대료가 현재보다 40% 낮아져야 한다는 결론이 나왔다. 28일로 예정된 2차 조정기일을 앞두고 전해진, 놀라운 소식이었다.


삼일회계법인은 현재 추정되는 출국객 수와 평균 구매 금액을 기반으로 매출 전망치를 산출했다. 그 결과, 출국객 증가로 연평균 4.5% 매출 성장이 예상되지만, 임대료 부담을 고려하면 손실이 더 커질 것으로 진단했다.


출국객 늘어도 매출은 ‘뚝’


IncheonAirport-DutyFree-Exit-Crisis-3-1024x584.jpg

인천공항 면세점 철수 위기 / 출처 : 연합뉴스


최근 출국객 수는 2019년 수준을 넘어 역대 최대치를 기록했지만, 면세점 매출은 여전히 코로나19 이전의 70~75% 수준에 머물고 있다.



매출 부진의 원인은 복합적이다. 먼저, 출국 수속이 3시간 이상이나 걸리는 공항 혼잡으로 많은 여행객들의 쇼핑 시간이 줄어들었다.


또한, 중국인 관광객의 소비 패턴 변화로 매출 비중이 2019년 63.1%에서 35.9%로 급감했다. 여기에 내국인들까지 온라인 면세점을 선호하면서 공항 매장 매출 하락세에 한층 더 불을 지폈다.


이런 상황에서 임대료 부담은 매출의 30~40%에 달한다. 신라면세점과 신세계면세점 측은 “지난해 매출 감소로 1,000억 원의 적자를 기록했다”며 “매년 손실이 누적되는 구조로는 사업을 지속하기 어렵다”고 호소했다.


롯데 면세점 철수에 이어


IncheonAirport-DutyFree-Exit-Crisis-4-1024x683.jpg

인천공항 면세점 철수 위기 / 출처 : 연합뉴스


이미 위험 신호는 감지됐었다. 2023년 인천공항 출국장 면세점 운영 사업자 선정 입찰에서 롯데면세점이 탈락하며, 22년 만에 완전히 철수했던 것이다. 높은 임대료와 월 최대 100억 원에 달하는 적자가 지속되는 상황에서 회사는 더 이상 버틸 수 없었다.


이제 신라면세점과 신세계면세점도 같은 기로에 서 있다. 이들의 법률대리인은 “28일 2차 조정을 통해 임대료가 합리적으로 조정되길 바란다”면서도 “조정이 실패하면 면세점 철수도 고려할 수밖에 없다”고 경고했다.



28일 열리는 2차 조정에서 인천국제공항공사가 임대료 조정에 응할지, 아니면 면세점들의 대량 철수라는 초유의 사태를 맞게 될지 귀추가 주목된다.

"""

통합 SNS 자동화 시스템 - 설정 GUI

Tkinter를 사용한 사용자 친화적 설정 도구 (디시인사이드, 국방일보 지원)


주요 기능:

- YAML 설정 파일 생성/편집

- 환경변수 파일 관리

- Chrome 프로필 경로 설정

- Airtable 연결 테스트

- 시스템 상태 모니터링

- 디시인사이드 갤러리 설정

- 국방일보(kookbangtimes) 전용 설정

"""


import tkinter as tk

from tkinter import ttk, filedialog, messagebox, scrolledtext

import os

import yaml

import threading

import time

from pathlib import Path

from typing import Dict, Any, Optional

import subprocess

import sys

from dataclasses import dataclass, asdict



@dataclass

class SiteSettings:

"""사이트 설정 정보"""

site_name: str = ""

execution_interval: int = 3600

platform_interval: int = 10

chrome_profiles: Dict[str, str] = None

airtable_env_file: str = ""

platforms: Dict[str, Dict[str, Any]] = None

logging_level: str = "INFO"

def __post_init__(self):

if self.chrome_profiles is None:

self.chrome_profiles = {

"band": "",

"brunch": "",

"snippod": "",

"kakao": "",

"dcinside": "" # 디시인사이드 추가

}

if self.platforms is None:

self.platforms = {

"band": {"enabled": True},

"brunch": {"enabled": True},

"snippod": {"enabled": True},

"kakao": {"enabled": True},

"dcinside": {"enabled": True} # 디시인사이드 추가

}



class ConfigGUI:

"""메인 설정 GUI 클래스"""

def __init__(self):

self.root = tk.Tk()

self.root.title("SNS 자동화 시스템 설정 (디시인사이드, 국방일보 포함)")

self.root.geometry("1200x900") # 창 크기 증가

self.root.resizable(True, True)

# 최소 크기 설정

self.root.minsize(1000, 700)

# 설정 데이터 - 국방일보 추가

self.sites = {

"withcar": SiteSettings("withcar", 3600),

"econmingle": SiteSettings("econmingle", 180),

"kookbangtimes": SiteSettings("kookbangtimes", 1800) # 국방일보 추가 (30분 간격)

}

# 현재 선택된 사이트

self.current_site = "withcar"

# GUI 구성 요소들

self.notebook = None

self.site_vars = {}

self.status_text = None

# 디렉토리 확인 및 생성

self._ensure_directories()

# 기존 설정 로드 시도

self._load_existing_configs()

# GUI 초기화

self._setup_gui()

# 스타일 설정

self._setup_styles()

def _ensure_directories(self):

"""필요한 디렉토리들 생성"""

directories = [

"integrated_sns_automation/config",

"env",

"logs",

"sites/withcar",

"sites/econmingle",

"sites/kookbangtimes" # 국방일보 디렉토리 추가

]

for directory in directories:

Path(directory).mkdir(parents=True, exist_ok=True)

def _load_existing_configs(self):

"""기존 설정 파일들 로드"""

for site_name in ["withcar", "econmingle", "kookbangtimes"]: # 국방일보 추가

config_file = Path(f"integrated_sns_automation/config/{site_name}_config.yaml")

if config_file.exists():

try:

with open(config_file, 'r', encoding='utf-8') as f:

config_data = yaml.safe_load(f)

if config_data:

site_settings = SiteSettings()

site_settings.site_name = config_data.get('site_name', site_name)

site_settings.execution_interval = config_data.get('execution_interval', 3600)

site_settings.platform_interval = config_data.get('platform_interval', 10)

site_settings.chrome_profiles = config_data.get('chrome_profiles', {})

site_settings.airtable_env_file = config_data.get('airtable', {}).get('env_file', f"env/{site_name}.env")

site_settings.platforms = config_data.get('platforms', {})

site_settings.logging_level = config_data.get('logging', {}).get('level', 'INFO')

self.sites[site_name] = site_settings

except Exception as e:

print(f"설정 파일 로드 실패 ({site_name}): {e}")

def _setup_styles(self):

"""GUI 스타일 설정"""

style = ttk.Style()

# 테마 설정

try:

style.theme_use('clam') # 깔끔한 테마

except:

pass

# 색상 설정

style.configure('Title.TLabel', font=('Arial', 12, 'bold'))

style.configure('Section.TLabel', font=('Arial', 10, 'bold'))

style.configure('Success.TLabel', foreground='green')

style.configure('Error.TLabel', foreground='red')

style.configure('Warning.TLabel', foreground='orange')

def _setup_gui(self):

"""GUI 초기화"""

# 메인 프레임

main_frame = ttk.Frame(self.root, padding="10")

main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

# 제목

title_label = ttk.Label(main_frame, text="SNS 자동화 시스템 설정 (디시인사이드, 국방일보 포함)", style='Title.TLabel')

title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))

# 탭 노트북

self.notebook = ttk.Notebook(main_frame)

self.notebook.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))

# 사이트별 탭 생성

self._create_site_tabs()

# 하단 버튼들

self._create_bottom_buttons(main_frame)

# 상태 표시 영역

self._create_status_area(main_frame)

# 그리드 가중치 설정

self.root.columnconfigure(0, weight=1)

self.root.rowconfigure(0, weight=1)

main_frame.columnconfigure(0, weight=1)

main_frame.rowconfigure(1, weight=1)

def _create_site_tabs(self):

"""사이트별 탭 생성"""

for site_name, site_settings in self.sites.items():

# 탭 이름 설정

if site_name == "kookbangtimes":

tab_text = "국방일보 설정"

else:

tab_text = f"{site_name.upper()} 설정"

tab_frame = ttk.Frame(self.notebook)

self.notebook.add(tab_frame, text=tab_text)

# 스크롤 가능한 프레임

canvas = tk.Canvas(tab_frame)

scrollbar = ttk.Scrollbar(tab_frame, orient="vertical", command=canvas.yview)

scrollable_frame = ttk.Frame(canvas)

canvas.configure(yscrollcommand=scrollbar.set)

canvas.bind('<Configure>', lambda e: canvas.configure(scrollregion=canvas.bbox("all")))

canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")

canvas.pack(side="left", fill="both", expand=True)

scrollbar.pack(side="right", fill="y")

# 사이트 설정 폼 생성

self._create_site_form(scrollable_frame, site_name, site_settings)

def _create_site_form(self, parent, site_name: str, site_settings: SiteSettings):

"""사이트별 설정 폼 생성"""

# 변수 초기화

if site_name not in self.site_vars:

self.site_vars[site_name] = {}

vars_dict = self.site_vars[site_name]

row = 0

# 패딩 추가

main_frame = ttk.Frame(parent, padding="20")

main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

# 사이트별 설명 추가

if site_name == "kookbangtimes":

desc_text = "국방일보 - 군사/국방 관련 콘텐츠 자동 공유"

ttk.Label(main_frame, text=desc_text, foreground="navy").grid(row=row, column=0, columnspan=3, sticky=tk.W, pady=(0, 10))

row += 1

# 기본 설정 섹션

ttk.Label(main_frame, text="기본 설정", style='Section.TLabel').grid(row=row, column=0, columnspan=3, sticky=tk.W, pady=(0, 10))

row += 1

# 사이트 이름

ttk.Label(main_frame, text="사이트 이름:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10), pady=8)

vars_dict['site_name'] = tk.StringVar(value=site_settings.site_name)

ttk.Entry(main_frame, textvariable=vars_dict['site_name'], state='readonly', width=25).grid(row=row, column=1, sticky=tk.W, pady=8)

row += 1

# 실행 간격

ttk.Label(main_frame, text="실행 간격:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10), pady=8)

interval_frame = ttk.Frame(main_frame)

interval_frame.grid(row=row, column=1, columnspan=2, sticky=tk.W, pady=8)

vars_dict['execution_interval'] = tk.IntVar(value=site_settings.execution_interval)

# 국방일보는 다른 간격 옵션 제공

if site_name == "kookbangtimes":

interval_values = [300, 600, 900, 1800, 3600, 7200] # 5분, 10분, 15분, 30분, 1시간, 2시간

else:

interval_values = [60, 180, 300, 600, 1800, 3600]

interval_combo = ttk.Combobox(interval_frame, textvariable=vars_dict['execution_interval'], width=15,

values=interval_values)

interval_combo.grid(row=0, column=0)

if site_name == "kookbangtimes":

ttk.Label(interval_frame, text="초 (300=5분, 1800=30분, 3600=1시간)").grid(row=0, column=1, padx=(10, 0))

else:

ttk.Label(interval_frame, text="초 (60=1분, 180=3분, 3600=1시간)").grid(row=0, column=1, padx=(10, 0))

row += 1

# 구분선

ttk.Separator(main_frame, orient='horizontal').grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20)

row += 1

# Chrome 프로필 섹션

ttk.Label(main_frame, text="Chrome 프로필 경로", style='Section.TLabel').grid(row=row, column=0, columnspan=3, sticky=tk.W, pady=(0, 10))

row += 1

platforms = ["band", "brunch", "snippod", "kakao", "dcinside"] # 디시인사이드 추가

vars_dict['chrome_profiles'] = {}

for platform in platforms:

ttk.Label(main_frame, text=f"{platform.upper()}:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10), pady=5)

path_frame = ttk.Frame(main_frame)

path_frame.grid(row=row, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5)

vars_dict['chrome_profiles'][platform] = tk.StringVar(

value=site_settings.chrome_profiles.get(platform, f"~/selenium_{site_name}_{platform}")

)

path_entry = ttk.Entry(path_frame, textvariable=vars_dict['chrome_profiles'][platform], width=60)

path_entry.grid(row=0, column=0, sticky=(tk.W, tk.E))

browse_btn = ttk.Button(path_frame, text="�", width=4,

command=lambda p=platform, sn=site_name: self._browse_chrome_profile(sn, p))

browse_btn.grid(row=0, column=1, padx=(10, 0))

path_frame.columnconfigure(0, weight=1)

row += 1

# 구분선

ttk.Separator(main_frame, orient='horizontal').grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20)

row += 1

# 플랫폼 활성화 섹션

ttk.Label(main_frame, text="플랫폼 활성화", style='Section.TLabel').grid(row=row, column=0, columnspan=3, sticky=tk.W, pady=(0, 10))

row += 1

vars_dict['platforms'] = {}

platform_frame = ttk.Frame(main_frame)

platform_frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)

for i, platform in enumerate(platforms):

vars_dict['platforms'][platform] = tk.BooleanVar(

value=site_settings.platforms.get(platform, {}).get('enabled', True)

)

cb = ttk.Checkbutton(platform_frame, text=platform.upper(),

variable=vars_dict['platforms'][platform])

cb.grid(row=0, column=i, padx=(0, 30), sticky=tk.W)

row += 1

# 국방일보 전용 설정 섹션

if site_name == "kookbangtimes":

# 구분선

ttk.Separator(main_frame, orient='horizontal').grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20)

row += 1

ttk.Label(main_frame, text="국방일보 전용 설정", style='Section.TLabel').grid(row=row, column=0, columnspan=3, sticky=tk.W, pady=(0, 10))

row += 1

ttk.Label(main_frame, text="기본 카테고리:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10), pady=8)

vars_dict['default_category'] = tk.StringVar(value="군대")

category_combo = ttk.Combobox(main_frame, textvariable=vars_dict['default_category'], width=20,

values=['군대', '국방', '안보', '정치', '사회', '경제', '세계', 'IT과학'])

category_combo.grid(row=row, column=1, sticky=tk.W, pady=8)

row += 1

# 디시인사이드 갤러리 정보 표시

dc_gallery_info = """

※ 디시인사이드 갤러리 추천:

- 군사 갤러리: military

- 국방 갤러리: defense

- 정치 갤러리: politics

"""

ttk.Label(main_frame, text=dc_gallery_info, foreground="blue").grid(row=row, column=0, columnspan=3, sticky=tk.W, pady=5)

row += 1

# 구분선

ttk.Separator(main_frame, orient='horizontal').grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20)

row += 1

# Airtable 설정 섹션

ttk.Label(main_frame, text="Airtable 설정", style='Section.TLabel').grid(row=row, column=0, columnspan=3, sticky=tk.W, pady=(0, 10))

row += 1

ttk.Label(main_frame, text="환경변수 파일:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10), pady=8)

env_frame = ttk.Frame(main_frame)

env_frame.grid(row=row, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=8)

vars_dict['env_file'] = tk.StringVar(value=site_settings.airtable_env_file or f"env/{site_name}.env")

env_entry = ttk.Entry(env_frame, textvariable=vars_dict['env_file'], width=50)

env_entry.grid(row=0, column=0, sticky=(tk.W, tk.E))

env_btn = ttk.Button(env_frame, text="편집", width=8,

command=lambda sn=site_name: self._edit_env_file(sn))

env_btn.grid(row=0, column=1, padx=(10, 0))

test_btn = ttk.Button(env_frame, text="테스트", width=8,

command=lambda sn=site_name: self._test_airtable_connection(sn))

test_btn.grid(row=0, column=2, padx=(10, 0))

env_frame.columnconfigure(0, weight=1)

row += 1

# 구분선

ttk.Separator(main_frame, orient='horizontal').grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20)

row += 1

# 로깅 설정

ttk.Label(main_frame, text="로깅 레벨:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10), pady=8)

vars_dict['logging_level'] = tk.StringVar(value=site_settings.logging_level)

log_combo = ttk.Combobox(main_frame, textvariable=vars_dict['logging_level'], width=20,

values=['DEBUG', 'INFO', 'WARNING', 'ERROR'])

log_combo.grid(row=row, column=1, sticky=tk.W, pady=8)

row += 1

# 여백 추가

ttk.Label(main_frame, text="").grid(row=row, column=0, pady=30)

main_frame.columnconfigure(1, weight=1)

parent.columnconfigure(0, weight=1)

def _browse_chrome_profile(self, site_name: str, platform: str):

"""Chrome 프로필 폴더 선택"""

current_path = self.site_vars[site_name]['chrome_profiles'][platform].get()

if current_path:

# 기존 경로가 있으면 그 디렉토리를 기본으로

initial_dir = os.path.dirname(os.path.expanduser(current_path))

else:

initial_dir = os.path.expanduser("~")

selected_path = filedialog.askdirectory(

title=f"{site_name} {platform} Chrome 프로필 폴더 선택",

initialdir=initial_dir

)

if selected_path:

self.site_vars[site_name]['chrome_profiles'][platform].set(selected_path)

def _edit_env_file(self, site_name: str):

"""환경변수 파일 편집"""

env_path = self.site_vars[site_name]['env_file'].get()

# 환경변수 파일 편집 창 열기

env_window = tk.Toplevel(self.root)

env_window.title(f"{site_name} 환경변수 편집")

env_window.geometry("700x600")

# 텍스트 에디터

text_frame = ttk.Frame(env_window)

text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

text_area = scrolledtext.ScrolledText(text_frame, wrap=tk.WORD, font=('Consolas', 10))

text_area.pack(fill=tk.BOTH, expand=True)

# 기존 파일 내용 로드

try:

if os.path.exists(env_path):

with open(env_path, 'r', encoding='utf-8') as f:

content = f.read()

else:

# 템플릿 내용 생성

content = self._generate_env_template(site_name)

text_area.insert('1.0', content)

except Exception as e:

text_area.insert('1.0', f"# 파일 로드 오류: {e}\n\n" + self._generate_env_template(site_name))

# 버튼 프레임

btn_frame = ttk.Frame(env_window)

btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10))

def save_env():

try:

# 디렉토리 생성

os.makedirs(os.path.dirname(env_path), exist_ok=True)

# 파일 저장

with open(env_path, 'w', encoding='utf-8') as f:

f.write(text_area.get('1.0', tk.END))

messagebox.showinfo("저장 완료", f"환경변수 파일이 저장되었습니다:\n{env_path}")

env_window.destroy()

except Exception as e:

messagebox.showerror("저장 실패", f"파일 저장 중 오류가 발생했습니다:\n{e}")

ttk.Button(btn_frame, text="저장", command=save_env).pack(side=tk.RIGHT, padx=(5, 0))

ttk.Button(btn_frame, text="취소", command=env_window.destroy).pack(side=tk.RIGHT)

def _generate_env_template(self, site_name: str) -> str:

"""환경변수 파일 템플릿 생성"""

if site_name == "kookbangtimes":

# 국방일보 전용 템플릿

template = f"""# KOOKBANGTIMES (국방일보) 환경변수 파일

# Airtable 설정

AIRTABLE_API_KEY=your_airtable_api_key_here

AIRTABLE_BASE_ID=your_airtable_base_id_here

AIRTABLE_TABLE_NAME=your_table_name_here


# 네이트 계정 (브런치용)

KOOKBANG_NATE_ID=your_nate_id_here

KOOKBANG_NATE_PASSWORD=your_nate_password_here


# 스니팟 계정

KOOKBANG_SNIPPOD_EMAIL=your_snippod_email_here

KOOKBANG_SNIPPOD_PASSWORD=your_snippod_password_here


# 카카오 계정

KOOKBANG_KAKAO_ID=your_kakao_id_here

KOOKBANG_KAKAO_PASSWORD=your_kakao_password_here


# 디시인사이드 설정 (군사/국방 갤러리 추천)

KOOKBANG_DC_GALLERY_ID=military # 갤러리 ID (예: military, defense, politics)

KOOKBANG_DC_IS_MINOR=false # 마이너 갤러리 여부 (true/false)

KOOKBANG_DC_USER_ID= # 디시 아이디 (선택사항, 수동 로그인 가능)

KOOKBANG_DC_PASSWORD= # 디시 비밀번호 (선택사항)


# 참고:

# - 실제 값으로 교체해주세요

# - 이 파일은 버전관리에 포함되지 않습니다

# - 보안을 위해 권한을 600으로 설정하는 것을 권장합니다

# - 디시인사이드는 수동 로그인도 가능합니다

# - 국방/군사 관련 갤러리를 추천합니다

"""

else:

# 기존 사이트 템플릿

prefix = site_name.upper() if site_name != "econmingle" else "ECON"

template = f"""# {site_name.upper()} 환경변수 파일

# Airtable 설정

AIRTABLE_API_KEY=your_airtable_api_key_here

AIRTABLE_BASE_ID=your_airtable_base_id_here

AIRTABLE_TABLE_NAME=your_table_name_here


# 네이트 계정

{prefix}_NATE_ID=your_nate_id_here

{prefix}_NATE_PASSWORD=your_nate_password_here


# 스니팟 계정

{prefix}_SNIPPOD_EMAIL=your_snippod_email_here

{prefix}_SNIPPOD_PASSWORD=your_snippod_password_here


# 카카오 계정

{prefix}_KAKAO_ID=your_kakao_id_here

{prefix}_KAKAO_PASSWORD=your_kakao_password_here


# 디시인사이드 설정

{prefix}_DC_GALLERY_ID=programming # 갤러리 ID (예: programming, car, economy)

{prefix}_DC_IS_MINOR=false # 마이너 갤러리 여부 (true/false)

{prefix}_DC_USER_ID= # 디시 아이디 (선택사항, 수동 로그인 가능)

{prefix}_DC_PASSWORD= # 디시 비밀번호 (선택사항)


# 참고:

# - 실제 값으로 교체해주세요

# - 이 파일은 버전관리에 포함되지 않습니다

# - 보안을 위해 권한을 600으로 설정하는 것을 권장합니다

# - 디시인사이드는 수동 로그인도 가능합니다

"""

return template

def _test_airtable_connection(self, site_name: str):

"""Airtable 연결 테스트"""

env_path = self.site_vars[site_name]['env_file'].get()

def test_connection():

try:

# 환경변수 로드

from dotenv import load_dotenv

load_dotenv(env_path)

# Airtable 연결 테스트

from pyairtable import Api

api_key = os.getenv('AIRTABLE_API_KEY')

base_id = os.getenv('AIRTABLE_BASE_ID')

table_name = os.getenv('AIRTABLE_TABLE_NAME')

if not all([api_key, base_id, table_name]):

raise ValueError("환경변수가 설정되지 않았습니다")

api = Api(api_key)

table = api.table(base_id, table_name)

# 테스트 조회

records = table.all(max_records=1)

self._update_status(f"✅ {site_name} Airtable 연결 성공!", "success")

except Exception as e:

self._update_status(f"❌ {site_name} Airtable 연결 실패: {e}", "error")

# 백그라운드에서 테스트 실행

threading.Thread(target=test_connection, daemon=True).start()

self._update_status(f"� {site_name} Airtable 연결 테스트 중...", "info")

def _create_bottom_buttons(self, parent):

"""하단 버튼들 생성"""

btn_frame = ttk.Frame(parent)

btn_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)

# 저장 버튼

ttk.Button(btn_frame, text="� 설정 저장", command=self._save_configs,

style='Accent.TButton').pack(side=tk.LEFT, padx=(0, 10))

# 시스템 실행 버튼

ttk.Button(btn_frame, text="� 시스템 시작", command=self._start_system).pack(side=tk.LEFT, padx=(0, 10))

# 로그 보기 버튼

ttk.Button(btn_frame, text="� 로그 보기", command=self._show_logs).pack(side=tk.LEFT, padx=(0, 10))

# 도움말 버튼

ttk.Button(btn_frame, text="❓ 도움말", command=self._show_help).pack(side=tk.RIGHT)

btn_frame.columnconfigure(0, weight=1)

def _create_status_area(self, parent):

"""상태 표시 영역 생성"""

status_frame = ttk.LabelFrame(parent, text="상태", padding="5")

status_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0))

self.status_text = scrolledtext.ScrolledText(status_frame, height=8, wrap=tk.WORD,

font=('Consolas', 9), state=tk.DISABLED,

bg='white', fg='black')

self.status_text.pack(fill=tk.BOTH, expand=True)

# 초기 상태 메시지

self._update_status("� SNS 자동화 시스템 설정 GUI가 시작되었습니다.", "info")

self._update_status("� 디시인사이드 플랫폼이 추가되었습니다!", "info")

self._update_status("�️ 국방일보(kookbangtimes) 사이트가 추가되었습니다!", "info")

self._update_status("� 각 탭에서 사이트별 설정을 완료한 후 '설정 저장'을 클릭하세요.", "info")

def _update_status(self, message: str, level: str = "info"):

"""상태 메시지 업데이트"""

if self.status_text:

timestamp = time.strftime("%H:%M:%S")

# 색상 태그 설정

if not hasattr(self, '_tags_configured'):

self.status_text.tag_config("info", foreground="blue")

self.status_text.tag_config("success", foreground="green")

self.status_text.tag_config("warning", foreground="orange")

self.status_text.tag_config("error", foreground="red")

self._tags_configured = True

self.status_text.config(state=tk.NORMAL)

self.status_text.insert(tk.END, f"[{timestamp}] {message}\n", level)

self.status_text.see(tk.END)

self.status_text.config(state=tk.DISABLED)

def _save_configs(self):

"""설정 파일들 저장"""

try:

for site_name, vars_dict in self.site_vars.items():

# YAML 설정 파일 생성

config_data = self._build_config_data(site_name, vars_dict)

# 파일 저장

config_path = f"integrated_sns_automation/config/{site_name}_config.yaml"

with open(config_path, 'w', encoding='utf-8') as f:

yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, indent=2)

self._update_status(f"✅ {site_name} 설정 파일 저장 완료: {config_path}", "success")

self._update_status("� 모든 설정 파일이 저장되었습니다!", "success")

messagebox.showinfo("저장 완료", "모든 설정이 성공적으로 저장되었습니다!")

except Exception as e:

error_msg = f"설정 저장 중 오류 발생: {e}"

self._update_status(f"❌ {error_msg}", "error")

messagebox.showerror("저장 실패", error_msg)

def _build_config_data(self, site_name: str, vars_dict: Dict) -> Dict[str, Any]:

"""설정 데이터 구성"""

# 기본 URL 및 카테고리 설정

site_configs = {

"withcar": {

"band_url": "https://www.band.us/band/90998336",

"kakao_url": "https://center-pf.kakao.com/_xgdNcxj/posts",

"default_category": "자동차",

"category_mapping": {

"newcar": "자동차",

"economy": "경제",

"society": "사회",

"news": "사회",

"military": "군대",

"default": "자동차"

}

},

"econmingle": {

"band_url": "https://www.band.us/band/92834125",

"kakao_url": "https://center-pf.kakao.com/_bBbKG/posts",

"default_category": "경제",

"category_mapping": {

"car": "자동차",

"economy": "경제",

"society": "사회",

"news": "사회",

"military": "군대",

"default": "경제"

}

},

"kookbangtimes": {

"band_url": "https://www.band.us/band/99685203", # 실제 밴드 ID로 교체 필요

"kakao_url": "https://center-pf.kakao.com/_exlMen/posts", # 실제 카카오 ID로 교체 필요

"default_category": "군대",

"category_mapping": {

"military": "군대",

"defense": "군대"

}

}

}

site_info = site_configs.get(site_name, site_configs["withcar"])

# 환경변수 접두사 설정

if site_name == "withcar":

env_prefix = "WITHCAR"

elif site_name == "econmingle":

env_prefix = "ECON"

elif site_name == "kookbangtimes":

env_prefix = "KOOKBANG"

else:

env_prefix = site_name.upper()

# 디시인사이드 설정 구성 (환경변수에서 읽기)

dc_config = {

"enabled": vars_dict['platforms']['dcinside'].get(),

"credentials_env": [f"{env_prefix}_DC_GALLERY_ID", f"{env_prefix}_DC_IS_MINOR",

f"{env_prefix}_DC_USER_ID", f"{env_prefix}_DC_PASSWORD"],

"manual_login_wait": 60,

"retry_attempts": 3

}

config_data = {

"site_name": site_name,

"execution_interval": vars_dict['execution_interval'].get(),

"platform_interval": 10,

"chrome_profiles": {

platform: vars_dict['chrome_profiles'][platform].get()

for platform in ["band", "brunch", "snippod", "kakao", "dcinside"]

},

"airtable": {

"env_file": vars_dict['env_file'].get(),

"api_key_env": "AIRTABLE_API_KEY",

"base_id_env": "AIRTABLE_BASE_ID",

"table_name_env": "AIRTABLE_TABLE_NAME"

},

"platforms": {

"band": {

"enabled": vars_dict['platforms']['band'].get(),

"target_url": site_info["band_url"],

"manual_login_wait": 60,

"retry_attempts": 3

},

"brunch": {

"enabled": vars_dict['platforms']['brunch'].get(),

"manual_login_wait": 60,

"retry_attempts": 3,

"category_mapping": site_info["category_mapping"]

},

"snippod": {

"enabled": vars_dict['platforms']['snippod'].get(),

"login_url": "https://snippod.com/login",

"credentials_env": [f"{env_prefix}_SNIPPOD_EMAIL", f"{env_prefix}_SNIPPOD_PASSWORD"],

"manual_login_wait": 60,

"retry_attempts": 3,

"category_mapping": site_info["category_mapping"],

"default_category": site_info.get("default_category", "자동차")

},

"kakao": {

"enabled": vars_dict['platforms']['kakao'].get(),

"target_url": site_info["kakao_url"],

"credentials_env": [f"{env_prefix}_KAKAO_ID", f"{env_prefix}_KAKAO_PASSWORD"],

"manual_login_wait": 60,

"retry_attempts": 3

},

"dcinside": dc_config # 디시인사이드 설정 추가

},

"logging": {

"level": vars_dict['logging_level'].get(),

"file": f"logs/{site_name}_auto_share.log",

"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",

"max_file_size": 10485760,

"backup_count": 5

}

}

# 국방일보 전용 설정 추가

if site_name == "kookbangtimes" and 'default_category' in vars_dict:

config_data['default_category'] = vars_dict['default_category'].get()

return config_data

def _start_system(self):

"""시스템 시작"""

def run_system():

try:

self._update_status("� SNS 자동화 시스템을 시작합니다...", "info")

# main_controller.py 실행

result = subprocess.run([sys.executable, "./integrated_sns_automation/main_controller.py"],

capture_output=True, text=True, timeout=10)

if result.returncode == 0:

self._update_status("✅ 시스템이 성공적으로 시작되었습니다!", "success")

else:

self._update_status(f"❌ 시스템 시작 실패: {result.stderr}", "error")

except subprocess.TimeoutExpired:

self._update_status("� 시스템이 백그라운드에서 실행 중입니다...", "info")

except Exception as e:

self._update_status(f"❌ 시스템 시작 오류: {e}", "error")

# 설정 파일 존재 확인

missing_configs = []

for site_name in ["withcar", "econmingle", "kookbangtimes"]:

if not os.path.exists(f"integrated_sns_automation/config/{site_name}_config.yaml"):

missing_configs.append(site_name)

if missing_configs:

messagebox.showwarning("설정 누락",

f"다음 사이트의 설정 파일이 없습니다: {', '.join(missing_configs)}\n"

"먼저 '설정 저장'을 클릭해주세요.")

return

# 백그라운드에서 실행

threading.Thread(target=run_system, daemon=True).start()

def _show_logs(self):

"""로그 보기 창"""

log_window = tk.Toplevel(self.root)

log_window.title("로그 보기")

log_window.geometry("900x600")

# 탭 노트북

notebook = ttk.Notebook(log_window)

notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

# 각 사이트별 로그 탭

for site_name in ["withcar", "econmingle", "kookbangtimes"]:

log_frame = ttk.Frame(notebook)

# 탭 이름 설정

if site_name == "kookbangtimes":

tab_text = "국방일보 로그"

else:

tab_text = f"{site_name.upper()} 로그"

notebook.add(log_frame, text=tab_text)

log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, font=('Consolas', 9))

log_text.pack(fill=tk.BOTH, expand=True)

# 로그 파일 읽기

log_file = f"logs/{site_name}_auto_share.log"

try:

if os.path.exists(log_file):

with open(log_file, 'r', encoding='utf-8') as f:

content = f.read()

# 최근 1000줄만 표시

lines = content.split('\n')

if len(lines) > 1000:

content = '\n'.join(lines[-1000:])

log_text.insert('1.0', content)

else:

log_text.insert('1.0', f"로그 파일이 없습니다: {log_file}")

except Exception as e:

log_text.insert('1.0', f"로그 파일 읽기 오류: {e}")

def _show_help(self):

"""도움말 표시"""

help_text = """

� SNS 자동화 시스템 설정 도움말 (디시인사이드, 국방일보 포함)


� 기본 설정:

• 사이트 이름: 변경할 수 없습니다

• 실행 간격: 시스템이 동작하는 주기입니다

- WithCar: 60분 권장 (자동차 관련)

- EconMingle: 3분 권장 (경제 관련)

- Kookbangtimes: 30분 권장 (군사/국방 관련)


� Chrome 프로필:

• 각 플랫폼별로 독립적인 Chrome 프로필이 필요합니다

• 브런치, 밴드, 스니팟, 카카오, 디시인사이드 5개 플랫폼 지원

• 폴더가 없으면 자동으로 생성됩니다


✅ 플랫폼 활성화:

• 체크된 플랫폼만 동작합니다

• 테스트 시에는 일부만 활성화하는 것을 권장합니다


�️ 국방일보(Kookbangtimes) 특별 기능:

• 군사/국방 관련 콘텐츠 전문

• 추천 디시인사이드 갤러리:

- military (군사 갤러리)

- defense (국방 갤러리)

- politics (정치 갤러리)

• 기본 카테고리: 군대


� 디시인사이드 설정:

• 환경변수 파일에서 설정합니다

• {PREFIX}_DC_GALLERY_ID: 갤러리 ID 설정

• {PREFIX}_DC_IS_MINOR: 마이너 갤러리 여부

• 수동 로그인도 가능합니다


� 환경변수 파일:

• 계정 정보가 저장되는 파일입니다

• '편집' 버튼으로 계정 정보를 입력하세요

• 보안을 위해 실제 값으로 교체해야 합니다


� 시스템 시작:

• 설정 저장 후 '시스템 시작'을 클릭하세요

• 백그라운드에서 자동으로 동작합니다


❓ 문제가 발생하면:

1. 로그 보기에서 오류 확인

2. 환경변수 파일의 계정 정보 확인

3. Chrome 프로필 경로 확인

4. Airtable 연결 테스트 실행

5. 디시인사이드 갤러리 ID 확인

"""

messagebox.showinfo("도움말", help_text)

def run(self):

"""GUI 실행"""

self.root.mainloop()



def main():

"""메인 함수"""

try:

app = ConfigGUI()

app.run()

except Exception as e:

print(f"GUI 실행 오류: {e}")

messagebox.showerror("오류", f"GUI 실행 중 오류가 발생했습니다:\n{e}")



if __name__ == "__main__":

main()

keyword
작가의 이전글전통시장에서 5만원 썼더니…정부가 “2000만원 드릴게