From 0cf79131698ca48e428bcb09fd08529376ba9d68 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 23 Mar 2026 22:48:08 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=94=84=EB=A6=AC=EB=AF=B8=EC=97=84=20?= =?UTF-8?q?=ED=88=B4=202=EC=A2=85=20+=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=83=81=ED=92=88=202=EC=A2=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - automation/page.tsx: 부동산 크롤러·회계 자동화 프리미엄 섹션 UI 추가 - accounting_automation_v1.0.py: 사업장 회계 장부 자동화 프로그램 생성 (5개 업종·19개 지출 항목·손익계산서·분기요약·부가세 자료 등 5시트 Excel 보고서) - prompt/page.tsx: 이미지 생성 프롬프트 패키지(45,000원) + 자소서·이력서 첨삭 프롬프트(35,000원) 상품 추가 (다크 그라디언트 카드, 실제 프롬프트 미리보기, ContactModal 연결) Co-Authored-By: Claude Sonnet 4.6 --- app/services/automation/page.tsx | 107 +- app/services/prompt/page.tsx | 191 +++ .../downloads/accounting_automation_v1.0.py | 1243 +++++++++++++++++ 3 files changed, 1540 insertions(+), 1 deletion(-) create mode 100644 public/downloads/accounting_automation_v1.0.py diff --git a/app/services/automation/page.tsx b/app/services/automation/page.tsx index 309f014..3400e94 100644 --- a/app/services/automation/page.tsx +++ b/app/services/automation/page.tsx @@ -58,6 +58,47 @@ const tools = [ }, ]; +const premiumTools = [ + { + id: 'realestate', + title: '부동산 매물 크롤링 프로그램', + subtitle: 'Real Estate Crawler v1.0', + desc: '직방·다방·피터팬·네이버부동산 4개 플랫폼을 동시 수집. 지역·거래유형·매물유형 선택 후 플랫폼별 시트로 Excel 자동 저장.', + tags: ['Python', '4개 플랫폼', '중복제거', 'Excel 출력'], + price: '3만원', + features: ['직방·다방·피터팬·네이버부동산 동시 수집', '지역·거래유형·매물유형 선택', '자동 중복 제거', '플랫폼별 시트 분리 Excel 저장'], + color: '#b45309', + bgFrom: '#451a03', + bgTo: '#78350f', + accentColor: '#f59e0b', + icon: ( + + + + ), + downloadPath: '/downloads/real_estate_crawler_v1.0.py', + }, + { + id: 'accounting', + title: '사업장 회계 장부 자동화 프로그램', + subtitle: 'Accounting Automation v1.0', + desc: '쇼핑몰·식당·제조업·서비스업 업종별 수입/지출 입력 → 매출총이익·영업이익·순이익 자동 계산 + 회계사 관점 조언 리포트.', + tags: ['Python', '업종별 맞춤', '손익계산서', '회계 조언'], + price: '5만원', + features: ['업종별 맞춤 항목 (쇼핑몰·식당·제조·서비스업)', '매출총이익·영업이익·순이익 자동 계산', '부가세 신고 준비 자료 자동 생성', '회계 전문가 관점 경고·조언 리포트'], + color: '#0f766e', + bgFrom: '#042f2e', + bgTo: '#134e4a', + accentColor: '#2dd4bf', + icon: ( + + + + ), + downloadPath: '/downloads/accounting_automation_v1.0.py', + }, +]; + const CHECKLIST = [ '자동화하고 싶은 업무를 구체적으로 설명해주세요', '현재 사용 중인 프로그램/시스템 (엑셀, ERP, 쇼핑몰 등)', @@ -252,7 +293,71 @@ export default function AutomationPage() { - {/* ─── 자동화 툴 다운로드 ─── */} + {/* ─── 프리미엄 툴 ─── */} +
+
+
+ + + PREMIUM TOOLS + +

프리미엄 자동화 프로그램

+

전문 분야별 고급 자동화 프로그램. 구매 후 소스코드 전달 + 1개월 무상 지원.

+
+
+ {premiumTools.map((tool) => ( +
+ {/* 카드 헤더 */} +
+
+
{tool.icon}
+
+ PREMIUM + {tool.price} +
+
+
{tool.subtitle}
+

{tool.title}

+

{tool.desc}

+
+ {/* 기능 목록 */} +
+
+ {tool.features.map((f) => ( +
+ + + + {f} +
+ ))} +
+
+ {/* 태그 + CTA */} +
+
+ {tool.tags.map((tag) => ( + {tag} + ))} +
+ +

구매 후 Python 소스코드 + 사용 가이드 전달

+
+
+ ))} +
+
+
+ + {/* ─── 자동화 툴 무료 다운로드 ─── */}
diff --git a/app/services/prompt/page.tsx b/app/services/prompt/page.tsx index 1684146..90edbd0 100644 --- a/app/services/prompt/page.tsx +++ b/app/services/prompt/page.tsx @@ -12,6 +12,113 @@ const CHECKLIST = [ '납품 후 사용 가이드 및 1:1 교육 포함 여부 확인', ]; +// ─── 프리미엄 상품 ─── +const premiumProducts = [ + { + id: 'image_gen', + badge: 'IMAGE GENERATION', + badgeColor: '#e879f9', + bgFrom: '#1a0533', + bgTo: '#2d1054', + accentColor: '#d946ef', + accentBg: 'rgba(217,70,239,0.12)', + accentBorder: 'rgba(217,70,239,0.3)', + title: 'AI 이미지 생성 마스터 프롬프트 패키지', + subtitle: 'Midjourney · DALL-E 3 · Stable Diffusion 전용', + price: '45,000원', + priceNote: '/ 패키지 (즉시 다운로드)', + desc: '수천 장의 이미지 생성 실험을 통해 검증된, 업종별·스타일별 고품질 프롬프트 50종 세트. 단순 키워드 나열이 아닌 구도·조명·분위기·카메라·후처리까지 세밀하게 설계된 전문가급 프롬프트입니다.', + features: [ + { label: '50종 프롬프트 라이브러리', desc: '상업용 · 마케팅 · SNS 콘텐츠 · 제품 사진 · 인물 포트레이트 · 배경 · 로고 컨셉 · 인테리어 등 카테고리별 구성' }, + { label: '구도·조명·후처리 공식', desc: '주제(Subject) → 환경(Environment) → 스타일(Style) → 조명(Lighting) → 카메라(Camera) → 후처리(Post) 6단계 구조 적용' }, + { label: '네거티브 프롬프트 포함', desc: '흐림·왜곡·불필요한 텍스트·비현실적 요소를 제거하는 부정 키워드까지 최적화하여 실패 확률 최소화' }, + { label: 'Midjourney 파라미터 완전 가이드', desc: '--ar / --v / --style / --chaos / --no 등 핵심 파라미터 활용법과 상황별 추천값 포함' }, + { label: '한국어 → 영어 변환 치트시트', desc: '자주 쓰는 한국어 표현을 AI가 잘 이해하는 영어 표현으로 매핑한 빠른 참고 시트' }, + { label: '업종별 특화 세트 5개', desc: '카페/음식 · 패션 · 부동산 인테리어 · 교육/강의 · 뷰티/헬스 분야 특화 프롬프트 세트별 제공' }, + { label: '활용 예시 이미지 50장', desc: '각 프롬프트로 실제 생성한 결과물 예시 이미지 포함 (PDF 가이드북 형태 제공)' }, + { label: '무제한 재사용 가능', desc: '구매 후 본인 사업·작업에 무제한 활용 가능. 상업적 이용 허용' }, + ], + promptPreview: { + title: '프롬프트 예시 — 프리미엄 카페 음료 사진', + content: `A professional product photograph of a single iced caramel latte in a tall clear glass, +placed on a rustic wooden cafe table. The drink features layers of espresso, milk, +and golden caramel syrup with ice cubes. + +Lighting: soft natural window light from the left, warm golden hour tone, +subtle rim lighting highlighting condensation droplets on the glass. + +Camera: Canon EOS R5, 85mm f/1.8 lens, shallow depth of field, +foreground blur with coffee beans and a sprig of dried lavender. + +Style: editorial food photography, Pinterest aesthetic, warm muted tones, +slightly desaturated with lifted shadows. + +Post-processing: film grain texture, subtle vignette, +color grade with warm highlights and cool shadows. + +--ar 4:5 --v 6.1 --style raw --q 2 + +Negative: text, watermark, multiple cups, cartoon, illustration, +overexposed, blurry, plastic look, artificial lighting`, + }, + cta: '패키지 구매 문의 →', + productId: 'prompt_image_gen', + }, + { + id: 'resume', + badge: 'CAREER COACHING', + badgeColor: '#34d399', + bgFrom: '#052e16', + bgTo: '#064e3b', + accentColor: '#10b981', + accentBg: 'rgba(16,185,129,0.12)', + accentBorder: 'rgba(16,185,129,0.3)', + title: 'AI 자소서·이력서 첨삭 마스터 프롬프트', + subtitle: 'ChatGPT · Claude 전용 · 대기업 HR 기준 적용', + price: '35,000원', + priceNote: '/ 패키지 (즉시 다운로드)', + desc: '7년차 대기업 재직 경험과 수십 명의 신입/경력 지원자 첨삭 경험을 바탕으로 설계한 자소서·이력서 최적화 프롬프트 세트. 합격률을 높이는 구체적인 표현과 구조로 AI가 전문 컨설턴트처럼 첨삭하도록 만들어드립니다.', + features: [ + { label: '자기소개서 7가지 유형별 프롬프트', desc: '지원동기 · 성장과정 · 강점/약점 · 직무역량 · 팀워크 경험 · 위기극복 · 입사 후 포부 — 각 항목에 최적화된 별도 프롬프트 제공' }, + { label: 'STAR 기법 자동 적용', desc: '상황(Situation) → 과제(Task) → 행동(Action) → 결과(Result) 구조로 경험을 임팩트 있게 재구성하는 프롬프트 포함' }, + { label: '이력서 불릿포인트 최적화', desc: '단순 업무 나열이 아닌 "무엇을 했고, 어떤 방법으로, 어떤 결과를 냈는지" 3단 구조 + 수치화로 강점을 극대화하는 첨삭 프롬프트' }, + { label: 'ATS 키워드 최적화', desc: '채용 공고의 키워드를 분석하여 자소서에 자연스럽게 녹여내는 ATS(지원자 추적 시스템) 통과 최적화 프롬프트' }, + { label: '업종/직무별 맞춤 톤 설정', desc: 'IT · 금융 · 제조 · 마케팅 · 공공기관 등 업종별, 신입/경력별 적합한 문체와 표현 스타일로 자동 조정' }, + { label: '약점을 강점으로 전환하는 프롬프트', desc: '공백기 · 전공 불일치 · 낮은 학점 · 짧은 재직기간 등 불리한 스펙을 긍정적으로 표현하는 전략적 첨삭 프롬프트' }, + { label: '면접 질문 예측 & 답변 준비', desc: '작성된 자소서를 기반으로 예상 면접 질문을 생성하고, 모범 답변 구조를 잡아주는 면접 대비 프롬프트 포함' }, + { label: '실제 첨삭 Before/After 5사례', desc: '실제로 사용하여 개선된 자소서 전후 비교 예시 5가지를 PDF로 제공 (직무별 다양한 케이스)' }, + ], + promptPreview: { + title: '프롬프트 예시 — 지원동기 항목 첨삭', + content: `당신은 15년 경력의 대기업 HR 수석 컨설턴트입니다. +수백 명의 합격 자소서를 분석한 전문가 관점에서 다음 자소서를 첨삭해주세요. + +[첨삭 기준] +1. STAR 기법(상황-과제-행동-결과) 구조가 명확한가? +2. 지원 동기가 회사의 사업 방향/가치와 구체적으로 연결되는가? +3. "열정", "성장" 등 추상적 단어 대신 구체적 경험과 수치가 있는가? +4. 첫 문장이 면접관의 시선을 끄는 Hook으로 시작하는가? +5. 지원 직무에서 필요한 역량이 자연스럽게 드러나는가? + +[첨삭 방식] +① 현재 자소서의 강점 2가지 (구체적 근거 포함) +② 치명적 약점 3가지와 개선 방향 +③ 표현이 약한 문장 3개를 지목하여 강화 버전으로 재작성 +④ 전체 구조 리뉴얼 버전 (300자 이내로 압축한 임팩트 버전) +⑤ 이 자소서로 예상되는 면접 질문 2가지 + +[지원 정보] +- 회사/직무: [입력] +- 채용 공고 키워드: [입력] + +[첨삭할 자소서] +[여기에 자소서 붙여넣기]`, + }, + cta: '패키지 구매 문의 →', + productId: 'prompt_resume', + }, +]; + const useCases = [ { label: '이메일 작성', desc: '고객사별, 상황별 최적화된 비즈니스 이메일 프롬프트' }, { label: '보고서·기획서', desc: '회사 내부 보고서, 제안서, 기획서 자동 작성용 프롬프트' }, @@ -133,6 +240,90 @@ export default function PromptPage() {
+ {/* ─── 프리미엄 상품 ─── */} +
+
+
+
+ + PREMIUM PRODUCTS +
+

바로 쓸 수 있는 프리미엄 프롬프트

+

전문가가 직접 설계하고 검증한 완성형 프롬프트 패키지 — 구매 즉시 사용 가능

+
+
+ {premiumProducts.map((product) => ( +
+ {/* 헤더 */} +
+
+ + {product.badge} + +
+
{product.price}
+
{product.priceNote}
+
+
+

{product.title}

+

{product.subtitle}

+

{product.desc}

+
+ {/* 기능 목록 */} +
+

포함 내용

+
    + {product.features.map((f, i) => ( +
  • +
    +
    +
    +
    + {f.label} + — {f.desc} +
    +
  • + ))} +
+
+ {/* 프롬프트 미리보기 */} +
+

+ {product.promptPreview.title} +

+
+ {product.promptPreview.content} +
+
+ {/* CTA */} +
+ +

+ 구매 문의 시 프롬프트 샘플 파일 미리 제공 +

+
+
+ ))} +
+
+
+ {/* ─── Before/After ─── */}
diff --git a/public/downloads/accounting_automation_v1.0.py b/public/downloads/accounting_automation_v1.0.py new file mode 100644 index 0000000..df807af --- /dev/null +++ b/public/downloads/accounting_automation_v1.0.py @@ -0,0 +1,1243 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +쟁승메이드 사업장 회계 장부 자동화 프로그램 v1.0 +문의: bgg8988@gmail.com | jaengseung-made.vercel.app +""" + +import os +import sys +import json +from datetime import datetime +from typing import Optional + +# 필수 패키지 설치 확인 +try: + import pandas as pd + import openpyxl + from openpyxl.styles import ( + Font, PatternFill, Alignment, Border, Side, numbers + ) + from openpyxl.utils import get_column_letter + from openpyxl.styles.numbers import FORMAT_NUMBER_COMMA_SEPARATED1 +except ImportError: + print("필수 패키지가 설치되어 있지 않습니다.") + print("다음 명령어를 실행하세요: pip install pandas openpyxl") + sys.exit(1) + + +# ───────────────────────────────────────────── +# 상수 정의 +# ───────────────────────────────────────────── + +BRAND_NAME = "쟁승메이드" +BRAND_EMAIL = "bgg8988@gmail.com" +BRAND_URL = "jaengseung-made.vercel.app" +VERSION = "v1.0" + +# 업종 정의 +INDUSTRIES = { + "1": "쇼핑몰/이커머스", + "2": "음식점/카페", + "3": "제조업", + "4": "서비스업/프리랜서", + "5": "기타", +} + +# 업종별 수입 카테고리 +INCOME_CATEGORIES = { + "쇼핑몰/이커머스": [ + ("product_sales", "제품판매"), + ("shipping_income", "배송비수입"), + ("refund_deduct", "반품환불차감 (마이너스 입력)"), + ], + "음식점/카페": [ + ("hall_sales", "홀매출"), + ("baemin_sales", "배달의민족 매출"), + ("coupang_sales", "쿠팡이츠 매출"), + ("yogiyo_sales", "요기요 매출"), + ("takeout_sales", "포장매출"), + ], + "제조업": [ + ("product_sales", "제품판매"), + ("b2b_sales", "B2B 납품"), + ("inventory_change", "원자재 재고변동"), + ], + "서비스업/프리랜서": [ + ("project_income", "프로젝트 수입"), + ("monthly_income", "월정액 수입"), + ("consulting_income","컨설팅 수입"), + ], + "기타": [ + ("sales_1", "매출1"), + ("sales_2", "매출2"), + ("other_income", "기타 수입"), + ], +} + +# 공통 지출 카테고리 +EXPENSE_CATEGORIES = [ + # 매출원가 + ("cogs_goods", "상품/재료비", "매출원가", "변동"), + ("cogs_outsource", "외주비", "매출원가", "변동"), + ("cogs_packaging", "포장비", "매출원가", "변동"), + # 인건비 + ("labor_salary", "급여", "인건비", "고정"), + ("labor_insurance", "4대보험", "인건비", "고정"), + ("labor_severance", "퇴직금적립", "인건비", "고정"), + # 임대료 + ("rent", "임대료/관리비", "임대료", "고정"), + # 공과금 + ("util_electricity", "전기요금", "공과금", "고정"), + ("util_water", "수도요금", "공과금", "고정"), + ("util_gas", "가스요금", "공과금", "고정"), + ("util_telecom", "통신비", "공과금", "고정"), + # 마케팅 + ("mkt_ads", "광고비", "마케팅", "변동"), + ("mkt_platform", "플랫폼수수료", "마케팅", "변동"), + # 세금 + ("tax_vat", "부가세", "세금", "변동"), + ("tax_income", "종합소득세", "세금", "변동"), + ("tax_local", "지방소득세", "세금", "변동"), + # 기타 + ("etc_supplies", "소모품비", "기타", "변동"), + ("etc_depreciation", "감가상각비", "기타", "고정"), + ("etc_insurance", "보험료", "기타", "고정"), +] + +MONTHS_KR = [ + "1월","2월","3월","4월","5월","6월", + "7월","8월","9월","10월","11월","12월", +] + +QUARTERS_KR = ["1분기 (1~3월)", "2분기 (4~6월)", "3분기 (7~9월)", "4분기 (10~12월)"] + + +# ───────────────────────────────────────────── +# 유틸리티 함수 +# ───────────────────────────────────────────── + +def print_banner(): + """프로그램 시작 배너 출력""" + print("\n" + "=" * 60) + print(f" {BRAND_NAME} 사업장 회계 장부 자동화 프로그램 {VERSION}") + print(f" 문의: {BRAND_EMAIL}") + print(f" 웹사이트: {BRAND_URL}") + print("=" * 60 + "\n") + + +def print_separator(title: str = ""): + """구분선 출력""" + if title: + print(f"\n{'─' * 20} {title} {'─' * 20}") + else: + print("─" * 60) + + +def input_int(prompt: str, default: int = 0) -> int: + """정수 입력 (기본값 지원)""" + while True: + raw = input(prompt).strip() + if raw == "" and default is not None: + return default + try: + return int(raw.replace(",", "")) + except ValueError: + print(" [오류] 숫자만 입력하세요. (천단위 콤마 허용)") + + +def input_float(prompt: str) -> float: + """실수 입력""" + while True: + raw = input(prompt).strip() + try: + return float(raw.replace(",", "")) + except ValueError: + print(" [오류] 숫자만 입력하세요.") + + +def fmt_krw(value: float) -> str: + """원화 포맷 (천단위 콤마)""" + if value < 0: + return f"-{abs(value):,.0f}원" + return f"{value:,.0f}원" + + +def fmt_pct(value: float) -> str: + """퍼센트 포맷""" + return f"{value:.1f}%" + + +def select_industry() -> str: + """업종 선택 메뉴""" + print_separator("업종 선택") + for key, name in INDUSTRIES.items(): + print(f" [{key}] {name}") + while True: + choice = input("\n업종을 선택하세요 (1~5): ").strip() + if choice in INDUSTRIES: + selected = INDUSTRIES[choice] + print(f" → '{selected}' 선택됨\n") + return selected + print(" [오류] 1~5 사이의 번호를 입력하세요.") + + +def select_year() -> int: + """분석 연도 선택""" + current_year = datetime.now().year + year = input_int(f"분석 연도를 입력하세요 (기본값: {current_year}): ", default=current_year) + return year + + +# ───────────────────────────────────────────── +# 데이터 수집 +# ───────────────────────────────────────────── + +def collect_data_manual(industry: str, year: int) -> dict: + """직접 입력 방식으로 월별 데이터 수집""" + income_cats = INCOME_CATEGORIES[industry] + data = { + "industry": industry, + "year": year, + "months": {}, + } + + print_separator(f"{year}년 월별 데이터 입력") + print(" ※ 금액은 원(₩) 단위로 입력하세요. (천단위 콤마 허용)") + print(" ※ 입력을 건너뛰려면 그냥 Enter를 누르세요 (0원 처리).\n") + + for month_idx in range(1, 13): + month_label = MONTHS_KR[month_idx - 1] + print(f"\n ── {year}년 {month_label} ──") + + month_data = {"income": {}, "expense": {}} + + # 수입 입력 + print(" [수입]") + for cat_key, cat_name in income_cats: + val = input_int(f" {cat_name}: ", default=0) + month_data["income"][cat_key] = val + + # 지출 입력 + print(" [지출]") + for cat_key, cat_name, cat_group, cost_type in EXPENSE_CATEGORIES: + val = input_int(f" {cat_name} ({cat_group}): ", default=0) + month_data["expense"][cat_key] = val + + data["months"][month_idx] = month_data + + return data + + +def collect_data_excel(industry: str, year: int) -> Optional[dict]: + """Excel 파일에서 데이터 가져오기""" + print_separator("Excel 파일에서 가져오기") + print(" ※ 먼저 메인 메뉴에서 '입력양식 생성'을 선택하여 양식을 만드세요.") + file_path = input(" 가져올 Excel 파일 경로를 입력하세요: ").strip().strip('"') + + if not os.path.exists(file_path): + print(f" [오류] 파일을 찾을 수 없습니다: {file_path}") + return None + + try: + print(" Excel 파일을 읽는 중...") + xl = pd.ExcelFile(file_path) + data = { + "industry": industry, + "year": year, + "months": {}, + } + income_cats = INCOME_CATEGORIES[industry] + + for month_idx in range(1, 13): + sheet_name = f"{month_idx}월" + if sheet_name not in xl.sheet_names: + # 해당 월 시트가 없으면 0으로 초기화 + month_data = { + "income": {k: 0 for k, _ in income_cats}, + "expense": {k: 0 for k, _, __, ___ in EXPENSE_CATEGORIES}, + } + data["months"][month_idx] = month_data + continue + + df = pd.read_excel(file_path, sheet_name=sheet_name, header=0) + # 양식: A열=항목키, B열=항목명, C열=금액 + df.columns = ["key", "name", "amount"] + df["amount"] = pd.to_numeric(df["amount"].fillna(0), errors="coerce").fillna(0) + amount_map = dict(zip(df["key"].astype(str), df["amount"])) + + month_data = { + "income": {k: int(amount_map.get(k, 0)) for k, _ in income_cats}, + "expense": {k: int(amount_map.get(k, 0)) for k, _, __, ___ in EXPENSE_CATEGORIES}, + } + data["months"][month_idx] = month_data + + print(f" [완료] {file_path} 파일에서 데이터를 가져왔습니다.") + return data + + except Exception as e: + print(f" [오류] Excel 파일 읽기 실패: {e}") + return None + + +# ───────────────────────────────────────────── +# 재무 계산 +# ───────────────────────────────────────────── + +def calc_month_financials(month_data: dict) -> dict: + """월별 재무 지표 계산""" + income = month_data["income"] + expense = month_data["expense"] + + # 총 매출 (반품은 마이너스로 입력됨) + total_revenue = sum(income.values()) + + # 매출원가 + cogs = ( + expense.get("cogs_goods", 0) + + expense.get("cogs_outsource", 0) + + expense.get("cogs_packaging", 0) + ) + + # 매출총이익 + gross_profit = total_revenue - cogs + + # 판매관리비 (인건비 + 임대료 + 공과금 + 마케팅 + 기타) + sg_and_a = ( + expense.get("labor_salary", 0) + + expense.get("labor_insurance", 0) + + expense.get("labor_severance", 0) + + expense.get("rent", 0) + + expense.get("util_electricity", 0) + + expense.get("util_water", 0) + + expense.get("util_gas", 0) + + expense.get("util_telecom", 0) + + expense.get("mkt_ads", 0) + + expense.get("mkt_platform", 0) + + expense.get("etc_supplies", 0) + + expense.get("etc_depreciation", 0) + + expense.get("etc_insurance", 0) + ) + + # 영업이익 + operating_profit = gross_profit - sg_and_a + + # 세금 + taxes = ( + expense.get("tax_vat", 0) + + expense.get("tax_income", 0) + + expense.get("tax_local", 0) + ) + + # 순이익 + net_profit = operating_profit - taxes + + # 총 지출 + total_expense = sum(expense.values()) + + # 고정비 / 변동비 분리 + fixed_cost = sum( + expense.get(k, 0) + for k, _, __, cost_type in EXPENSE_CATEGORIES + if cost_type == "고정" + ) + variable_cost = sum( + expense.get(k, 0) + for k, _, __, cost_type in EXPENSE_CATEGORIES + if cost_type == "변동" + ) + + # 비율 계산 (0 나누기 방지) + def safe_pct(numerator, denominator): + if denominator == 0: + return 0.0 + return round(numerator / denominator * 100, 2) + + gross_margin = safe_pct(gross_profit, total_revenue) + operating_margin = safe_pct(operating_profit, total_revenue) + net_margin = safe_pct(net_profit, total_revenue) + labor_ratio = safe_pct( + expense.get("labor_salary", 0) + expense.get("labor_insurance", 0) + expense.get("labor_severance", 0), + total_revenue + ) + mkt_ratio = safe_pct( + expense.get("mkt_ads", 0) + expense.get("mkt_platform", 0), + total_revenue + ) + + # 손익분기점 (변동비율 기반) + variable_ratio = safe_pct(variable_cost, total_revenue) / 100 + if variable_ratio < 1: + bep = round(fixed_cost / (1 - variable_ratio)) + else: + bep = 0 # 계산 불가 + + return { + "total_revenue": total_revenue, + "cogs": cogs, + "gross_profit": gross_profit, + "sg_and_a": sg_and_a, + "operating_profit": operating_profit, + "taxes": taxes, + "net_profit": net_profit, + "total_expense": total_expense, + "fixed_cost": fixed_cost, + "variable_cost": variable_cost, + "gross_margin": gross_margin, + "operating_margin": operating_margin, + "net_margin": net_margin, + "labor_ratio": labor_ratio, + "mkt_ratio": mkt_ratio, + "bep": bep, + } + + +def calc_all_financials(data: dict) -> dict: + """전체 월별 재무 계산 후 분기/연간 집계""" + results = {} + for month_idx, month_data in data["months"].items(): + results[month_idx] = calc_month_financials(month_data) + + # 분기별 합산 + quarters = {} + for q in range(1, 5): + month_range = range((q - 1) * 3 + 1, q * 3 + 1) + q_data = { + key: sum(results[m][key] for m in month_range if m in results) + for key in results[1].keys() + if key not in ("gross_margin", "operating_margin", "net_margin", "labor_ratio", "mkt_ratio", "bep") + } + # 분기 비율 재계산 + rev = q_data["total_revenue"] + q_data["gross_margin"] = round(q_data["gross_profit"] / rev * 100, 2) if rev else 0 + q_data["operating_margin"] = round(q_data["operating_profit"] / rev * 100, 2) if rev else 0 + q_data["net_margin"] = round(q_data["net_profit"] / rev * 100, 2) if rev else 0 + q_data["labor_ratio"] = 0 + q_data["mkt_ratio"] = 0 + q_data["bep"] = 0 + quarters[q] = q_data + + # 연간 합산 + annual = { + key: sum(results[m][key] for m in results) + for key in results[1].keys() + if key not in ("gross_margin", "operating_margin", "net_margin", "labor_ratio", "mkt_ratio", "bep") + } + rev = annual["total_revenue"] + annual["gross_margin"] = round(annual["gross_profit"] / rev * 100, 2) if rev else 0 + annual["operating_margin"] = round(annual["operating_profit"] / rev * 100, 2) if rev else 0 + annual["net_margin"] = round(annual["net_profit"] / rev * 100, 2) if rev else 0 + annual["labor_ratio"] = 0 + annual["mkt_ratio"] = 0 + annual["bep"] = 0 + + return {"monthly": results, "quarterly": quarters, "annual": annual} + + +# ───────────────────────────────────────────── +# 회계 전문가 조언 엔진 +# ───────────────────────────────────────────── + +def generate_advice(data: dict, financials: dict) -> list: + """규칙 기반 회계 조언 생성""" + advice_list = [] + annual = financials["annual"] + year = data["year"] + + # 1. 마진율 경보 + if annual["gross_margin"] < 20: + advice_list.append({ + "category": "수익성 경고", + "priority": "긴급", + "title": f"매출총이익률 {fmt_pct(annual['gross_margin'])} — 위험 수준", + "detail": ( + "매출총이익률이 20% 미만입니다. 원가율이 너무 높아 사업 지속성이 위협받을 수 있습니다.\n" + "• 원재료/상품 공급처 재협상 또는 대체 공급처 탐색\n" + "• 저마진 제품/서비스 라인 구조조정 검토\n" + "• 판매가격 인상 가능 여부 시장 조사 필요" + ), + }) + elif annual["gross_margin"] < 35: + advice_list.append({ + "category": "수익성 주의", + "priority": "주의", + "title": f"매출총이익률 {fmt_pct(annual['gross_margin'])} — 개선 필요", + "detail": ( + "매출총이익률이 35% 미만으로 업종 평균 대비 낮은 수준입니다.\n" + "• 고부가가치 제품/서비스 비중 확대 전략 수립\n" + "• 원가 절감 가능 항목 면밀히 검토" + ), + }) + + # 2. 인건비 비율 + labor_total = ( + sum( + data["months"][m]["expense"].get("labor_salary", 0) + + data["months"][m]["expense"].get("labor_insurance", 0) + + data["months"][m]["expense"].get("labor_severance", 0) + for m in data["months"] + ) + ) + labor_ratio = labor_total / annual["total_revenue"] * 100 if annual["total_revenue"] else 0 + if labor_ratio > 30: + advice_list.append({ + "category": "비용 구조", + "priority": "주의", + "title": f"인건비 비율 {fmt_pct(labor_ratio)} — 업무 자동화 검토 필요", + "detail": ( + "인건비가 매출의 30%를 초과합니다. 생산성 향상 방안을 모색하세요.\n" + "• RPA, 엑셀 자동화 등 업무 자동화 도입으로 인건비 절감\n" + "• 아웃소싱·프리랜서 활용으로 고정 인건비를 변동비화\n" + "• 직원별 생산성 KPI 측정 및 성과급 체계 도입" + ), + }) + + # 3. 마케팅비 비율 + mkt_total = sum( + data["months"][m]["expense"].get("mkt_ads", 0) + + data["months"][m]["expense"].get("mkt_platform", 0) + for m in data["months"] + ) + mkt_ratio = mkt_total / annual["total_revenue"] * 100 if annual["total_revenue"] else 0 + if mkt_ratio > 10: + advice_list.append({ + "category": "마케팅 효율", + "priority": "주의", + "title": f"마케팅비 비율 {fmt_pct(mkt_ratio)} — ROI 분석 필요", + "detail": ( + "마케팅비가 매출의 10%를 초과합니다. 채널별 ROI를 정밀 분석하세요.\n" + "• 채널별 전환율·CAC(고객획득비용) 측정 체계 구축\n" + "• 효과 낮은 광고 채널 예산 축소, 고ROI 채널에 집중 투자\n" + "• 리타겟팅·CRM을 통한 재구매율 향상으로 CAC 절감" + ), + }) + + # 4. 순이익 적자 판단 + loss_months = [m for m, f in financials["monthly"].items() if f["net_profit"] < 0] + if len(loss_months) >= 6: + advice_list.append({ + "category": "경영 위기", + "priority": "긴급", + "title": f"연간 {len(loss_months)}개월 순손실 — 고정비 구조 즉시 재검토", + "detail": ( + f"총 {len(loss_months)}개월 순손실이 발생했습니다. 사업 구조 전면 재검토가 필요합니다.\n" + "• 임대료 협상 또는 이전을 통한 고정비 절감\n" + "• 손실 유발 서비스/제품 라인 즉시 중단 또는 개편\n" + "• 단기 자금 확보: 소상공인 정책자금, 신용보증기금 활용 검토" + ), + }) + elif annual["net_profit"] < 0: + advice_list.append({ + "category": "수익성 경고", + "priority": "긴급", + "title": f"연간 순손실 {fmt_krw(annual['net_profit'])} — 고정비 구조 재검토", + "detail": ( + "연간 순손실이 발생했습니다. 고정비 절감이 최우선 과제입니다.\n" + "• 임대료·인건비 등 고정비 재협상\n" + "• 불필요한 구독 서비스, 보험료 등 점검" + ), + }) + + # 5. 부가세 신고 안내 + advice_list.append({ + "category": "세무 일정", + "priority": "정보", + "title": "부가세 신고 시기 안내", + "detail": ( + f"■ {year}년 부가세 신고 일정\n" + f" • 1기 확정신고: {year}년 7월 1일 ~ 7월 25일 (1~6월 매출분)\n" + f" • 2기 확정신고: {year + 1}년 1월 1일 ~ 1월 25일 (7~12월 매출분)\n\n" + "■ 준비 서류\n" + " • 세금계산서 합계표 (매입/매출)\n" + " • 신용카드 매출전표 합계표\n" + " • 현금영수증 발행·수취 내역\n\n" + " ※ 홈택스(hometax.go.kr)에서 전자신고 가능" + ), + }) + + # 6. 세금 납부 시뮬레이션 (종합소득세) + taxable_income = annual["net_profit"] + if taxable_income > 0: + # 2024년 기준 종합소득세 세율 구간 + tax_brackets = [ + (14_000_000, 0.06, 0), + (50_000_000, 0.15, 1_260_000), + (88_000_000, 0.24, 5_760_000), + (150_000_000, 0.35, 15_440_000), + (300_000_000, 0.38, 19_940_000), + (500_000_000, 0.40, 25_940_000), + (1_000_000_000, 0.42, 35_940_000), + (float("inf"), 0.45, 65_940_000), + ] + estimated_tax = 0 + for limit, rate, deduction in tax_brackets: + if taxable_income <= limit: + estimated_tax = int(taxable_income * rate - deduction) + break + local_tax = int(estimated_tax * 0.1) + advice_list.append({ + "category": "세금 시뮬레이션", + "priority": "정보", + "title": f"종합소득세 예상 납부액: {fmt_krw(estimated_tax + local_tax)}", + "detail": ( + f"■ {year}년 종합소득세 시뮬레이션 (과세표준: {fmt_krw(taxable_income)})\n" + f" • 종합소득세: {fmt_krw(estimated_tax)}\n" + f" • 지방소득세 (10%): {fmt_krw(local_tax)}\n" + f" • 합계 예상 납부액: {fmt_krw(estimated_tax + local_tax)}\n\n" + " ※ 각종 공제항목에 따라 실제 납부액은 달라집니다.\n" + " ※ 세무사 상담을 통해 절세 전략을 수립하세요." + ), + }) + + return advice_list + + +# ───────────────────────────────────────────── +# 콘솔 요약 출력 +# ───────────────────────────────────────────── + +def print_summary(data: dict, financials: dict, advice_list: list): + """콘솔에 연간 요약 출력""" + annual = financials["annual"] + year = data["year"] + + print_separator(f"{year}년 연간 재무 요약") + print(f" 업종 : {data['industry']}") + print(f" 총 매출 : {fmt_krw(annual['total_revenue'])}") + print(f" 총 지출 : {fmt_krw(annual['total_expense'])}") + print(f" 매출총이익 : {fmt_krw(annual['gross_profit'])} ({fmt_pct(annual['gross_margin'])})") + print(f" 영업이익 : {fmt_krw(annual['operating_profit'])} ({fmt_pct(annual['operating_margin'])})") + print(f" 순이익 : {fmt_krw(annual['net_profit'])} ({fmt_pct(annual['net_margin'])})") + + print_separator("분기별 순이익") + for q in range(1, 5): + q_data = financials["quarterly"][q] + profit = q_data["net_profit"] + sign = "▲" if profit >= 0 else "▼" + print(f" {QUARTERS_KR[q-1]}: {sign} {fmt_krw(profit)}") + + print_separator("회계사 조언 요약") + for i, adv in enumerate(advice_list, 1): + print(f" [{adv['priority']}] {i}. {adv['title']}") + + +# ───────────────────────────────────────────── +# Excel 스타일 헬퍼 +# ───────────────────────────────────────────── + +HEADER_FILL = PatternFill(start_color="1D4ED8", end_color="1D4ED8", fill_type="solid") +ALT_FILL = PatternFill(start_color="EFF6FF", end_color="EFF6FF", fill_type="solid") +PROFIT_FONT = Font(color="1D4ED8", bold=True) +LOSS_FONT = Font(color="DC2626", bold=True) +HEADER_FONT = Font(color="FFFFFF", bold=True, size=11) +BORDER_SIDE = Side(style="thin", color="CBD5E1") +THIN_BORDER = Border( + left=BORDER_SIDE, right=BORDER_SIDE, + top=BORDER_SIDE, bottom=BORDER_SIDE, +) +NUM_FMT = '#,##0"원"' +PCT_FMT = '0.0"%"' + + +def style_header_row(ws, row_num: int, num_cols: int): + """헤더 행 스타일 적용""" + for col in range(1, num_cols + 1): + cell = ws.cell(row=row_num, column=col) + cell.fill = HEADER_FILL + cell.font = HEADER_FONT + cell.alignment = Alignment(horizontal="center", vertical="center") + cell.border = THIN_BORDER + + +def style_data_cell(cell, value, is_alt_row: bool = False, is_amount: bool = True, is_pct: bool = False): + """데이터 셀 스타일 적용""" + cell.value = value + cell.border = THIN_BORDER + cell.alignment = Alignment(horizontal="right" if (is_amount or is_pct) else "left", vertical="center") + if is_alt_row: + cell.fill = ALT_FILL + if is_amount and isinstance(value, (int, float)): + cell.number_format = NUM_FMT + if value < 0: + cell.font = LOSS_FONT + elif value > 0: + cell.font = PROFIT_FONT + elif is_pct and isinstance(value, (int, float)): + cell.number_format = PCT_FMT + + +def auto_col_width(ws, min_width: int = 10): + """열 너비 자동 조정""" + for col in ws.columns: + max_len = min_width + col_letter = get_column_letter(col[0].column) + for cell in col: + try: + cell_len = len(str(cell.value)) if cell.value else 0 + if cell_len > max_len: + max_len = cell_len + except Exception: + pass + ws.column_dimensions[col_letter].width = min(max_len + 4, 30) + + +# ───────────────────────────────────────────── +# Excel 보고서 생성 +# ───────────────────────────────────────────── + +def create_report_excel(data: dict, financials: dict, advice_list: list, output_path: str): + """Excel 보고서 생성 (5개 시트)""" + wb = openpyxl.Workbook() + wb.remove(wb.active) # 기본 시트 제거 + + _sheet1_monthly_pl(wb, data, financials) + _sheet2_quarterly(wb, data, financials) + _sheet3_cost_structure(wb, data, financials) + _sheet4_advice(wb, data, advice_list) + _sheet5_vat(wb, data, financials) + + wb.save(output_path) + print(f"\n [완료] 보고서가 저장되었습니다: {output_path}") + + +def _sheet1_monthly_pl(wb, data: dict, financials: dict): + """시트1: 월별 손익계산서""" + ws = wb.create_sheet("월별 손익계산서") + year = data["year"] + + # 타이틀 + ws.merge_cells("A1:N1") + title_cell = ws["A1"] + title_cell.value = f"{year}년 월별 손익계산서 — {data['industry']}" + title_cell.font = Font(bold=True, size=14) + title_cell.alignment = Alignment(horizontal="center") + + # 헤더 + headers = ["항목"] + [f"{m}월" for m in range(1, 13)] + ["연간 합계"] + for col_idx, h in enumerate(headers, 1): + ws.cell(row=2, column=col_idx, value=h) + style_header_row(ws, 2, len(headers)) + + # 항목 정의 + rows = [ + ("총 매출", "total_revenue", True, False), + ("매출원가", "cogs", True, False), + ("매출총이익", "gross_profit", True, False), + (" 매출총이익률", "gross_margin", False, True), + ("판매관리비", "sg_and_a", True, False), + ("영업이익", "operating_profit", True, False), + (" 영업이익률", "operating_margin", False, True), + ("세금", "taxes", True, False), + ("순이익", "net_profit", True, False), + (" 순이익률", "net_margin", False, True), + ("고정비", "fixed_cost", True, False), + ("변동비", "variable_cost", True, False), + ("손익분기점", "bep", True, False), + ] + + annual = financials["annual"] + for row_offset, (label, key, is_amount, is_pct) in enumerate(rows): + row_num = row_offset + 3 + is_alt = row_offset % 2 == 0 + + # 항목명 + name_cell = ws.cell(row=row_num, column=1, value=label) + name_cell.border = THIN_BORDER + name_cell.alignment = Alignment(horizontal="left") + if is_alt: + name_cell.fill = ALT_FILL + if key in ("gross_profit", "operating_profit", "net_profit"): + name_cell.font = Font(bold=True) + + # 월별 값 + for m in range(1, 13): + val = financials["monthly"][m][key] + cell = ws.cell(row=row_num, column=m + 1) + style_data_cell(cell, val, is_alt, is_amount, is_pct) + + # 연간 합계 + ann_val = annual[key] + ann_cell = ws.cell(row=row_num, column=14) + style_data_cell(ann_cell, ann_val, is_alt, is_amount, is_pct) + if key in ("gross_profit", "operating_profit", "net_profit"): + ann_cell.font = Font( + color="1D4ED8" if ann_val >= 0 else "DC2626", + bold=True, size=11 + ) + + # 틀 고정 (헤더 + 항목명) + ws.freeze_panes = "B3" + auto_col_width(ws, min_width=8) + ws.column_dimensions["A"].width = 16 + + +def _sheet2_quarterly(wb, data: dict, financials: dict): + """시트2: 분기별 요약""" + ws = wb.create_sheet("분기별 요약") + year = data["year"] + + ws.merge_cells("A1:F1") + ws["A1"].value = f"{year}년 분기별 재무 요약" + ws["A1"].font = Font(bold=True, size=14) + ws["A1"].alignment = Alignment(horizontal="center") + + headers = ["항목", "1분기 (1~3월)", "2분기 (4~6월)", "3분기 (7~9월)", "4분기 (10~12월)", "연간 합계"] + for col_idx, h in enumerate(headers, 1): + ws.cell(row=2, column=col_idx, value=h) + style_header_row(ws, 2, 6) + + keys = [ + ("총 매출", "total_revenue", True, False), + ("매출원가", "cogs", True, False), + ("매출총이익", "gross_profit", True, False), + (" 이익률", "gross_margin", False, True), + ("영업이익", "operating_profit", True, False), + (" 이익률", "operating_margin", False, True), + ("순이익", "net_profit", True, False), + (" 이익률", "net_margin", False, True), + ] + annual = financials["annual"] + for row_offset, (label, key, is_amount, is_pct) in enumerate(keys): + row_num = row_offset + 3 + is_alt = row_offset % 2 == 0 + + name_cell = ws.cell(row=row_num, column=1, value=label) + name_cell.border = THIN_BORDER + name_cell.alignment = Alignment(horizontal="left") + if is_alt: + name_cell.fill = ALT_FILL + + for q in range(1, 5): + val = financials["quarterly"][q][key] + cell = ws.cell(row=row_num, column=q + 1) + style_data_cell(cell, val, is_alt, is_amount, is_pct) + + ann_cell = ws.cell(row=row_num, column=6) + style_data_cell(ann_cell, annual[key], is_alt, is_amount, is_pct) + + ws.freeze_panes = "B3" + auto_col_width(ws, min_width=12) + ws.column_dimensions["A"].width = 14 + + +def _sheet3_cost_structure(wb, data: dict, financials: dict): + """시트3: 비용 구조 분석""" + ws = wb.create_sheet("비용 구조 분석") + year = data["year"] + annual_revenue = financials["annual"]["total_revenue"] + + ws.merge_cells("A1:E1") + ws["A1"].value = f"{year}년 비용 구조 분석 — 항목별 금액 및 매출 비율" + ws["A1"].font = Font(bold=True, size=14) + ws["A1"].alignment = Alignment(horizontal="center") + + headers = ["비용 항목", "분류", "성격", "연간 금액", "매출 비율"] + for col_idx, h in enumerate(headers, 1): + ws.cell(row=2, column=col_idx, value=h) + style_header_row(ws, 2, 5) + + # 항목별 연간 합산 + annual_expenses = {} + for m, month_data in data["months"].items(): + for k, v in month_data["expense"].items(): + annual_expenses[k] = annual_expenses.get(k, 0) + v + + for row_offset, (cat_key, cat_name, cat_group, cost_type) in enumerate(EXPENSE_CATEGORIES): + row_num = row_offset + 3 + is_alt = row_offset % 2 == 0 + amount = annual_expenses.get(cat_key, 0) + ratio = (amount / annual_revenue * 100) if annual_revenue else 0 + + for col_idx, val in enumerate([cat_name, cat_group, cost_type, amount, ratio], 1): + cell = ws.cell(row=row_num, column=col_idx) + is_amount = col_idx == 4 + is_pct = col_idx == 5 + style_data_cell(cell, val, is_alt, is_amount, is_pct) + if col_idx <= 3: + cell.alignment = Alignment(horizontal="left") + + # 합계 행 + total_row = len(EXPENSE_CATEGORIES) + 3 + total_expense = sum(annual_expenses.values()) + total_ratio = (total_expense / annual_revenue * 100) if annual_revenue else 0 + + ws.cell(row=total_row, column=1, value="합 계").font = Font(bold=True) + total_amt_cell = ws.cell(row=total_row, column=4, value=total_expense) + total_amt_cell.number_format = NUM_FMT + total_amt_cell.font = Font(bold=True) + total_pct_cell = ws.cell(row=total_row, column=5, value=total_ratio) + total_pct_cell.number_format = PCT_FMT + total_pct_cell.font = Font(bold=True) + + for col in range(1, 6): + ws.cell(row=total_row, column=col).border = THIN_BORDER + ws.cell(row=total_row, column=col).fill = PatternFill(start_color="DBEAFE", end_color="DBEAFE", fill_type="solid") + + auto_col_width(ws, min_width=10) + ws.column_dimensions["A"].width = 16 + ws.freeze_panes = "A3" + + +def _sheet4_advice(wb, data: dict, advice_list: list): + """시트4: 회계사 조언 리포트""" + ws = wb.create_sheet("회계사 조언 리포트") + year = data["year"] + + ws.merge_cells("A1:D1") + ws["A1"].value = f"{year}년 회계 전문가 조언 리포트 — {data['industry']}" + ws["A1"].font = Font(bold=True, size=14) + ws["A1"].alignment = Alignment(horizontal="center") + + ws.merge_cells("A2:D2") + ws["A2"].value = f"생성일시: {datetime.now().strftime('%Y-%m-%d %H:%M')} | {BRAND_NAME} ({BRAND_URL})" + ws["A2"].font = Font(size=10, color="64748B") + ws["A2"].alignment = Alignment(horizontal="center") + + headers = ["No.", "분류", "우선순위", "조언 제목 및 상세 내용"] + for col_idx, h in enumerate(headers, 1): + ws.cell(row=3, column=col_idx, value=h) + style_header_row(ws, 3, 4) + + priority_colors = { + "긴급": "FEE2E2", + "주의": "FEF3C7", + "정보": "EFF6FF", + } + priority_font_colors = { + "긴급": "DC2626", + "주의": "D97706", + "정보": "1D4ED8", + } + + current_row = 4 + for i, adv in enumerate(advice_list, 1): + # 상세 내용은 줄 수에 따라 행 병합 + detail_lines = adv["detail"].split("\n") + row_height = max(len(detail_lines) * 15, 40) + + for col_idx in range(1, 5): + cell = ws.cell(row=current_row, column=col_idx) + cell.border = THIN_BORDER + fill_color = priority_colors.get(adv["priority"], "FFFFFF") + cell.fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type="solid") + + ws.cell(row=current_row, column=1, value=i).alignment = Alignment(horizontal="center", vertical="top") + ws.cell(row=current_row, column=2, value=adv["category"]).alignment = Alignment(horizontal="center", vertical="top") + + pri_cell = ws.cell(row=current_row, column=3, value=adv["priority"]) + pri_cell.alignment = Alignment(horizontal="center", vertical="top") + pri_cell.font = Font( + color=priority_font_colors.get(adv["priority"], "000000"), + bold=True + ) + + content = f"■ {adv['title']}\n\n{adv['detail']}" + content_cell = ws.cell(row=current_row, column=4, value=content) + content_cell.alignment = Alignment( + horizontal="left", vertical="top", wrap_text=True + ) + + ws.row_dimensions[current_row].height = row_height + current_row += 1 + + ws.column_dimensions["A"].width = 5 + ws.column_dimensions["B"].width = 14 + ws.column_dimensions["C"].width = 10 + ws.column_dimensions["D"].width = 70 + ws.freeze_panes = "A4" + + +def _sheet5_vat(wb, data: dict, financials: dict): + """시트5: 부가세 신고 준비 자료""" + ws = wb.create_sheet("부가세 신고 준비") + year = data["year"] + + ws.merge_cells("A1:F1") + ws["A1"].value = f"{year}년 부가세 신고 준비 자료" + ws["A1"].font = Font(bold=True, size=14) + ws["A1"].alignment = Alignment(horizontal="center") + + # 1기 (1~6월) / 2기 (7~12월) 분리 + periods = { + f"1기 (1~6월) — {year}년 7월 신고": range(1, 7), + f"2기 (7~12월) — {year + 1}년 1월 신고": range(7, 13), + } + + income_cats = INCOME_CATEGORIES[data["industry"]] + current_row = 3 + + for period_label, month_range in periods.items(): + # 기간 헤더 + ws.merge_cells(f"A{current_row}:F{current_row}") + period_cell = ws.cell(row=current_row, column=1, value=period_label) + period_cell.fill = PatternFill(start_color="1E40AF", end_color="1E40AF", fill_type="solid") + period_cell.font = Font(color="FFFFFF", bold=True, size=12) + period_cell.alignment = Alignment(horizontal="center") + current_row += 1 + + # 소계 헤더 + headers = ["구분", "항목", "과세표준(공급가액)", "세율", "부가세액", "비고"] + for col_idx, h in enumerate(headers, 1): + ws.cell(row=current_row, column=col_idx, value=h) + style_header_row(ws, current_row, 6) + current_row += 1 + + # 매출 내역 + period_revenue = 0 + for cat_key, cat_name in income_cats: + period_amount = sum( + data["months"][m]["income"].get(cat_key, 0) + for m in month_range if m in data["months"] + ) + supply_value = int(period_amount / 1.1) # 공급가액 (부가세 포함 금액 기준) + vat = period_amount - supply_value + period_revenue += period_amount + + is_alt = (current_row % 2 == 0) + for col_idx in range(1, 7): + cell = ws.cell(row=current_row, column=col_idx) + cell.border = THIN_BORDER + if is_alt: + cell.fill = ALT_FILL + + ws.cell(row=current_row, column=1, value="매출").alignment = Alignment(horizontal="center") + ws.cell(row=current_row, column=2, value=cat_name) + supply_cell = ws.cell(row=current_row, column=3, value=supply_value) + supply_cell.number_format = NUM_FMT + ws.cell(row=current_row, column=4, value=0.10).number_format = PCT_FMT + vat_cell = ws.cell(row=current_row, column=5, value=vat) + vat_cell.number_format = NUM_FMT + ws.cell(row=current_row, column=6, value="과세") + current_row += 1 + + # 매입 세금계산서 관련 비용 + vatable_expenses = [ + ("cogs_goods", "상품/재료비"), + ("cogs_outsource","외주비"), + ("mkt_ads", "광고비"), + ("etc_supplies", "소모품비"), + ] + for exp_key, exp_name in vatable_expenses: + period_amount = sum( + data["months"][m]["expense"].get(exp_key, 0) + for m in month_range if m in data["months"] + ) + if period_amount == 0: + continue + supply_value = int(period_amount / 1.1) + vat = period_amount - supply_value + + is_alt = (current_row % 2 == 0) + for col_idx in range(1, 7): + cell = ws.cell(row=current_row, column=col_idx) + cell.border = THIN_BORDER + if is_alt: + cell.fill = ALT_FILL + + ws.cell(row=current_row, column=1, value="매입").alignment = Alignment(horizontal="center") + ws.cell(row=current_row, column=2, value=exp_name) + supply_cell = ws.cell(row=current_row, column=3, value=supply_value) + supply_cell.number_format = NUM_FMT + ws.cell(row=current_row, column=4, value=0.10).number_format = PCT_FMT + vat_cell = ws.cell(row=current_row, column=5, value=vat) + vat_cell.number_format = NUM_FMT + ws.cell(row=current_row, column=6, value="매입공제") + current_row += 1 + + current_row += 2 # 간격 + + auto_col_width(ws, min_width=10) + ws.column_dimensions["A"].width = 8 + ws.column_dimensions["B"].width = 16 + ws.column_dimensions["F"].width = 12 + + +# ───────────────────────────────────────────── +# 입력 양식 Excel 생성 +# ───────────────────────────────────────────── + +def create_input_template(industry: str, year: int, output_path: str): + """업종별 입력 양식 Excel 생성""" + wb = openpyxl.Workbook() + wb.remove(wb.active) + income_cats = INCOME_CATEGORIES[industry] + + for month_idx in range(1, 13): + sheet_name = f"{month_idx}월" + ws = wb.create_sheet(sheet_name) + + # 타이틀 + ws.merge_cells("A1:C1") + ws["A1"].value = f"{year}년 {MONTHS_KR[month_idx - 1]} 입력 양식 — {industry}" + ws["A1"].font = Font(bold=True, size=12) + ws["A1"].alignment = Alignment(horizontal="center") + + # 컬럼 헤더 + for col_idx, h in enumerate(["항목 키", "항목명", "금액 (원)"], 1): + ws.cell(row=2, column=col_idx, value=h) + style_header_row(ws, 2, 3) + + # 수입 섹션 + ws.cell(row=3, column=1, value="──── 수입 ────") + ws.cell(row=3, column=1).font = Font(bold=True, color="1D4ED8") + ws.merge_cells(f"A3:C3") + ws["A3"].fill = PatternFill(start_color="DBEAFE", end_color="DBEAFE", fill_type="solid") + ws["A3"].alignment = Alignment(horizontal="center") + + row = 4 + for cat_key, cat_name in income_cats: + ws.cell(row=row, column=1, value=cat_key) + ws.cell(row=row, column=2, value=cat_name) + amount_cell = ws.cell(row=row, column=3, value=0) + amount_cell.number_format = NUM_FMT + for col in range(1, 4): + ws.cell(row=row, column=col).border = THIN_BORDER + row += 1 + + # 지출 섹션 + ws.cell(row=row, column=1, value="──── 지출 ────") + ws.merge_cells(f"A{row}:C{row}") + ws[f"A{row}"].font = Font(bold=True, color="DC2626") + ws[f"A{row}"].fill = PatternFill(start_color="FEE2E2", end_color="FEE2E2", fill_type="solid") + ws[f"A{row}"].alignment = Alignment(horizontal="center") + row += 1 + + for cat_key, cat_name, cat_group, cost_type in EXPENSE_CATEGORIES: + ws.cell(row=row, column=1, value=cat_key) + ws.cell(row=row, column=2, value=f"[{cat_group}] {cat_name} ({cost_type})") + amount_cell = ws.cell(row=row, column=3, value=0) + amount_cell.number_format = NUM_FMT + for col in range(1, 4): + ws.cell(row=row, column=col).border = THIN_BORDER + row += 1 + + ws.column_dimensions["A"].width = 20 + ws.column_dimensions["B"].width = 30 + ws.column_dimensions["C"].width = 18 + ws.freeze_panes = "A3" + + wb.save(output_path) + print(f" [완료] 입력 양식이 생성되었습니다: {output_path}") + print(" ※ C열 (금액) 셀에 숫자를 입력하고 저장하세요.") + + +# ───────────────────────────────────────────── +# 메인 실행 흐름 +# ───────────────────────────────────────────── + +def main(): + print_banner() + + # 작업 디렉토리 설정 + work_dir = os.getcwd() + print(f" 작업 디렉토리: {work_dir}\n") + + # ── 메인 메뉴 ── + while True: + print_separator("메인 메뉴") + print(" [1] 새 회계 분석 시작") + print(" [2] 입력 양식(Excel) 생성") + print(" [0] 종료") + choice = input("\n선택하세요: ").strip() + + if choice == "0": + print("\n 프로그램을 종료합니다. 감사합니다!\n") + break + + elif choice == "2": + # 입력 양식 생성 + industry = select_industry() + year = select_year() + template_path = os.path.join( + work_dir, + f"입력양식_{industry}_{year}.xlsx" + ) + create_input_template(industry, year, template_path) + + elif choice == "1": + # 분석 시작 + industry = select_industry() + year = select_year() + + # 데이터 입력 방식 선택 + print_separator("데이터 입력 방식") + print(" [A] 직접 입력 (프로그램에서 월별 금액 입력)") + print(" [B] Excel 파일에서 가져오기") + input_method = input("\n선택하세요 (A/B): ").strip().upper() + + data = None + if input_method == "B": + data = collect_data_excel(industry, year) + if data is None: # A 선택 or B 실패 시 직접 입력 + if input_method == "B": + print(" → 직접 입력 방식으로 전환합니다.") + data = collect_data_manual(industry, year) + + # 재무 계산 + print("\n 재무 지표를 계산하는 중...") + financials = calc_all_financials(data) + + # 조언 생성 + advice_list = generate_advice(data, financials) + + # 콘솔 요약 출력 + print_summary(data, financials, advice_list) + + # Excel 보고서 출력 + timestamp = datetime.now().strftime("%Y%m%d_%H%M") + report_path = os.path.join( + work_dir, + f"회계보고서_{industry}_{year}_{timestamp}.xlsx" + ) + + print_separator("Excel 보고서 생성") + print(f" 보고서 저장 경로: {report_path}") + create_report_excel(data, financials, advice_list, report_path) + + # 완료 메시지 + print_separator("완료") + print(f" 보고서가 성공적으로 생성되었습니다!") + print(f" 파일: {report_path}") + print("\n 포함된 시트:") + print(" • 시트1: 월별 손익계산서 (12개월)") + print(" • 시트2: 분기별 요약") + print(" • 시트3: 비용 구조 분석") + print(" • 시트4: 회계사 조언 리포트") + print(" • 시트5: 부가세 신고 준비 자료") + + # 데이터 저장 여부 (JSON 백업) + save_json = input("\n 입력 데이터를 JSON으로 백업하시겠습니까? (y/N): ").strip().lower() + if save_json == "y": + json_path = report_path.replace(".xlsx", "_data.json") + with open(json_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + print(f" [완료] 데이터 백업: {json_path}") + + else: + print(" [오류] 0, 1, 2 중 하나를 입력하세요.") + + +# ───────────────────────────────────────────── +# 진입점 +# ───────────────────────────────────────────── + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\n [중단] 사용자가 프로그램을 종료했습니다.") + print(" 저장된 데이터는 유지됩니다.\n") + sys.exit(0) + except Exception as e: + print(f"\n [오류] 예기치 않은 오류가 발생했습니다: {e}") + print(" 문의: bgg8988@gmail.com\n") + sys.exit(1) + + +# ═══════════════════════════════════════════════════════════════ +# 쟁승메이드 (JaengseungMade) — 프리미엄 개발 서비스 +# ▸ 웹사이트 : jaengseung-made.vercel.app +# ▸ 이메일 : bgg8988@gmail.com +# ▸ 연락처 : 010-3907-1392 +# 7년차 대기업 백엔드 개발자가 만드는 실전 자동화 솔루션 +# ═══════════════════════════════════════════════════════════════