Files
jaengseung-made/public/downloads/accounting_automation_v1.0.py
gahusb 0cf7913169 feat: 프리미엄 툴 2종 + 프롬프트 상품 2종 추가
- 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 <noreply@anthropic.com>
2026-03-23 22:48:08 +09:00

1244 lines
50 KiB
Python

#!/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년차 대기업 백엔드 개발자가 만드는 실전 자동화 솔루션
# ═══════════════════════════════════════════════════════════════