날짜별 받아야 할 파일이 모두 왔는지 확인하기
파일이 세 개 있다고 해서 받아야 할 세 날짜가 모두 채워진 것은 아닙니다. 받기로 정한 날짜 목록과 실제 파일명의 날짜를 대조하면 누락된 날짜와 예상 밖의 파일을 따로 찾을 수 있습니다. 이번 가상 예제에서는 10월 1·2·5일의 파일을 받아야 하지만, 폴더에는 1·5·6일 파일이 있습니다. 결과는 2일 누락 한 건, 검사 기간 밖인 6일 파일 한 건입니다.
검사는 일반 파일의 존재와 파일명 날짜만 확인합니다. 파일 내용이 정상인지, 전송이 끝났는지, 해당 날짜의 실제 업무가 완료됐는지는 확인하지 않습니다. 파일을 옮기거나 이름을 바꾸지 않고, 검사 폴더 바깥에 새 JSON 보고서 한 개를 저장합니다.
설명용 도해: 필수 3일 중 1·5일 파일은 확인하고 2일은 누락으로 구분합니다. 6일은 범위 밖(outside_range)이며 예상 밖 1개입니다. 파일명과 존재만 대조하며 내용과 전송 완료는 확인하지 않습니다.
1 받아야 할 날짜를 CSV로 먼저 정합니다
실습 ZIP을 빈 연습 폴더에 풀고 fixtures/calendar.csv를 확인합니다. 날짜와 비수신일은 모두 가상 규칙입니다. 실제 공휴일이나 현재 수신 일정으로 해석하지 마세요.
id,date,required
D01,2026-10-01,yes
D02,2026-10-02,yes
D03,2026-10-03,no
D04,2026-10-04,no
D05,2026-10-05,yes
required=yes인 세 날짜만 필수 수신일입니다. no인 3·4일은 파일이 없어도 누락으로 세지 않습니다. 코드는 요일이나 공휴일을 계산하지 않으며 이 CSV의 표시를 그대로 사용합니다. 토요일이라도 CSV에 yes로 적으면 받아야 할 날짜로 검사합니다. 목록에 없는 날짜를 자동으로 필수 날짜에 추가하지도 않습니다.
달력의 id는 각 규칙 행을 구별하는 식별자입니다. 영문자로 시작하는 영문·숫자·밑줄·하이픈 조합이며 최대 32자입니다. ID와 날짜는 각각 중복할 수 없고, required에는 소문자 yes 또는 no만 넣습니다. 빈칸이나 잘못된 날짜를 임의로 보정하지 않습니다.

2 실제 파일명과 검사 범위를 고정합니다
fixtures/received 바로 아래에는 다음 일반 파일 세 개가 있습니다.
daily_2026-10-01_A.csvdaily_2026-10-05_A.csvdaily_2026-10-06_A.csv
파일명 규칙은 daily_YYYY-MM-DD_BATCH.csv입니다. 앞의 daily_, 날짜의 두 하이픈, 소문자 .csv를 정확히 맞춥니다. BATCH는 영문 대소문자 또는 숫자 1~8자입니다. 이름의 A 같은 일괄ID는 달력의 D01 같은 행 ID와 서로 대조하지 않습니다. 이 실습에서 수신을 묶는 기준은 날짜이며, 일괄ID는 내용 검증이나 거래 식별을 뜻하지 않습니다.
같은 날짜의 A와 B 파일이 함께 있으면 어느 파일을 사용할지 정하지 않고 날짜 중복으로 보류합니다. 이름만 다른 두 파일을 자동으로 한 건의 정상 수신으로 합치지 않습니다.
검사 한 번의 수신 단위는 필수 날짜당 파일명 규칙에 맞는 일반 파일 한 개입니다. 여러 부서에서 같은 날 각각 한 개씩 받아야 하는 월말 업무라면 날짜만으로 전체 수신을 판정할 수 없습니다. 한 부서의 파일만 있어도 그 날짜가 확인될 수 있고, 두 부서의 정상 파일이 함께 있으면 날짜 중복으로 분류될 수 있습니다. 파일명의 BATCH도 부서별 필수 목록과 대조하지 않습니다.
이 코드를 그대로 쓸 때는 발신자와 자료 종류가 확인된 작업 사본을 구분하고, 각 범위마다 받아야 할 날짜 CSV와 새 보고서를 따로 둡니다. 원래 수신 폴더는 보존하세요. 어느 부서의 어떤 자료인지 확인할 근거가 없거나 같은 날짜에 여러 역할의 파일이 필요한 경우에는 담당자와 수신 단위를 먼저 정합니다. 파일을 지우거나 날짜를 바꿔 중복 표시를 없애는 방식으로 통과시키지 않습니다.
검사 기간은 시작일과 종료일을 포함합니다. 이번에는 2026-10-01부터 2026-10-05까지이므로 6일 파일은 outside_range입니다. 기간 안의 no 날짜 파일은 no_delivery, 달력에 없는 날짜 파일은 undeclared_date로 기록합니다. 세 이유는 서로 배타적으로 적용하므로 파일 한 개를 예상 밖 합계에 두 번 넣지 않습니다.
달력은 UTF-8 CSV이고 머리글은 id,date,required 순서입니다. 입력은 65,536바이트 이하, 머리글 외 1~366행, 검사 기간은 순방향 1~366일로 제한합니다. 날짜는 2000~2099년의 YYYY-MM-DD 형식이며 달력의 모든 날짜는 검사 기간 안에 있어야 합니다. 검사 폴더 바로 아래 항목은 폴더와 링크까지 포함해 최대 200개입니다. 이 한도는 작은 연습 자료를 위한 범위입니다.
3 폴더 바로 아래 이름만 읽는 전체 코드
다음 코드는 ZIP의 code/check_received.py와 같습니다. Python 3.12 표준 라이브러리만 사용하며 패키지 설치가 필요하지 않습니다.
Python 날짜 공식 문서의 date.fromisoformat()은 여러 ISO 날짜 표기를 받습니다. 여기서는 먼저 정규식으로 YYYY-MM-DD 형식을 제한한 뒤 실제 존재하는 날짜인지 확인합니다. 따라서 20261002를 몰래 하이픈 형식으로 바꾸거나 2월 30일을 넘겨 처리하지 않습니다.
폴더 조회는 Path.iterdir 공식 문서의 바로 아래 항목 순회를 사용합니다. Path.lstat 공식 문서에 따라 링크 자체의 종류를 확인하고, 일반 파일만 날짜 대조에 넣습니다. 하위 폴더 안으로 들어가지 않으며 심볼릭 링크와 Windows 연결 지점도 따라가지 않습니다. 달력 입력·검사 폴더·보고서 경로의 상위 구성요소에 링크 또는 연결 지점이 있으면 실행을 거부합니다.
"""Compare a synthetic delivery calendar with top-level regular filenames."""
import argparse
import csv
from datetime import date
import hashlib
import io
import json
import os
from pathlib import Path
import re
import stat
import sys
DATE_PATTERN = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}\Z")
ID_PATTERN = re.compile(r"[A-Za-z][A-Za-z0-9_-]{0,31}\Z")
FILE_PATTERN = re.compile(
r"daily_([0-9]{4}-[0-9]{2}-[0-9]{2})_([A-Za-z0-9]{1,8})\.csv\Z"
)
MAX_CALENDAR_BYTES = 65_536
MAX_CALENDAR_ROWS = 366
MAX_ENTRIES = 200
def checked_date(text):
if not DATE_PATTERN.fullmatch(text):
raise ValueError("날짜 형식은 YYYY-MM-DD여야 합니다")
try:
value = date.fromisoformat(text)
except ValueError as error:
raise ValueError("달력에 존재하지 않는 날짜입니다: " + text) from error
if not 2000 <= value.year <= 2099:
raise ValueError("실습 날짜는 2000~2099년으로 제한합니다")
return value
def plain_path(value):
path = Path(os.path.abspath(value))
for part in (path, *path.parents):
if part.is_symlink() or part.is_junction():
raise ValueError("링크 또는 연결 지점 경로는 사용하지 않습니다: " + str(part))
return path
def read_calendar(path, start, end):
if not stat.S_ISREG(path.lstat().st_mode):
raise ValueError("달력 입력은 일반 파일이어야 합니다")
with path.open("rb") as handle:
raw = handle.read(MAX_CALENDAR_BYTES + 1)
if len(raw) > MAX_CALENDAR_BYTES:
raise ValueError("달력 입력 한도 65,536바이트를 초과했습니다")
try:
text = raw.decode("utf-8-sig", errors="strict")
except UnicodeDecodeError as error:
raise ValueError("달력 입력의 UTF-8 해석에 실패했습니다") from error
rows, ids, dates = [], set(), set()
try:
reader = csv.reader(io.StringIO(text, newline=""), strict=True)
if next(reader, None) != ["id", "date", "required"]:
raise ValueError("달력 머리글은 id,date,required 순서여야 합니다")
for record, fields in enumerate(reader, 2):
if len(fields) != 3:
raise ValueError(f"레코드 {record}: 열 수는 3개여야 합니다")
ident, raw_date, required = fields
if not ID_PATTERN.fullmatch(ident):
raise ValueError(f"레코드 {record}: 달력 ID 형식 확인")
if ident in ids:
raise ValueError("중복 달력 ID: " + ident)
day = checked_date(raw_date)
if raw_date in dates:
raise ValueError("중복 달력 날짜: " + raw_date)
if not start <= day <= end:
raise ValueError("검사 기간 밖의 달력 날짜: " + raw_date)
if required not in ("yes", "no"):
raise ValueError(f"레코드 {record}: required는 yes 또는 no여야 합니다")
ids.add(ident)
dates.add(raw_date)
rows.append({"id": ident, "date": raw_date, "required": required,
"source_record": record})
if len(rows) > MAX_CALENDAR_ROWS:
raise ValueError("달력 행 한도 366행을 초과했습니다")
except csv.Error as error:
raise ValueError("달력 CSV 구문 오류: " + str(error)) from error
if not rows:
raise ValueError("달력은 머리글 외에 1행 이상 필요합니다")
return sorted(rows, key=lambda row: row["date"]), hashlib.sha256(raw).hexdigest()
def inspect(calendar, folder, start_text, end_text):
start, end = checked_date(start_text), checked_date(end_text)
if end < start or (end - start).days + 1 > 366:
raise ValueError("검사 기간은 순방향 1~366일이어야 합니다")
if not stat.S_ISDIR(folder.lstat().st_mode):
raise ValueError("검사 대상은 일반 폴더여야 합니다")
rows, calendar_sha = read_calendar(calendar, start, end)
declared = {row["date"]: row for row in rows}
expected = sorted(day for day, row in declared.items() if row["required"] == "yes")
no_delivery = sorted(day for day, row in declared.items() if row["required"] == "no")
entries = []
for path in folder.iterdir():
entries.append(path)
if len(entries) > MAX_ENTRIES:
raise ValueError("폴더 바로 아래 항목 한도 200개를 초과했습니다")
observed, unexpected, invalid, held = {}, [], [], []
for path in sorted(entries, key=lambda item: item.name):
mode = path.lstat().st_mode
if stat.S_ISLNK(mode) or path.is_junction():
held.append({"name": path.name, "kind": "symlink"})
continue
if not stat.S_ISREG(mode):
kind = "directory" if stat.S_ISDIR(mode) else "special"
held.append({"name": path.name, "kind": kind})
continue
match = FILE_PATTERN.fullmatch(path.name)
if match is None:
invalid.append({"name": path.name, "reason": "pattern_mismatch"})
continue
raw_date, batch_id = match.groups()
try:
day = checked_date(raw_date)
except ValueError:
invalid.append({"name": path.name, "reason": "invalid_date"})
continue
item = {"name": path.name, "date": raw_date, "batch_id": batch_id}
observed.setdefault(raw_date, []).append(item)
if not start <= day <= end:
reason = "outside_range"
elif raw_date not in declared:
reason = "undeclared_date"
elif declared[raw_date]["required"] == "no":
reason = "no_delivery"
else:
continue
unexpected.append({**item, "reason": reason})
missing = [day for day in expected if day not in observed]
duplicates = [{"date": day, "names": [item["name"] for item in items]}
for day, items in sorted(observed.items()) if len(items) > 1]
received = [{"calendar_id": declared[day]["id"], **observed[day][0]}
for day in expected if len(observed.get(day, [])) == 1]
return {
"schema_version": 1, "period": {"start": start_text, "end": end_text},
"filename_pattern": "daily_YYYY-MM-DD_BATCH.csv; BATCH=1..8 ASCII letters/digits",
"scan_policy": "top-level regular files only; no recursion or link following",
"calendar_sha256": calendar_sha, "calendar_rows": rows,
"expected_dates": expected, "no_delivery_dates": no_delivery,
"received_dates": received, "missing_dates": missing,
"unexpected_files": unexpected, "invalid_filenames": invalid,
"held_entries": held, "duplicate_dates": duplicates,
"summary": {"expected_dates": len(expected), "received_dates": len(received),
"missing_dates": len(missing), "unexpected_files": len(unexpected),
"invalid_filenames": len(invalid), "held_entries": len(held),
"duplicate_dates": len(duplicates)},
"all_expected_present": not missing and all(len(observed.get(day, [])) == 1
for day in expected),
"content_checked": False, "transfer_completion_checked": False,
}
def write_report(calendar_text, folder_text, start_text, end_text, report_text):
calendar = plain_path(calendar_text)
folder = plain_path(folder_text)
target = plain_path(report_text)
if target == calendar:
raise ValueError("입력 달력과 보고서는 서로 다른 경로여야 합니다")
if target == folder or folder in target.parents:
raise ValueError("보고서는 검사 폴더 바깥에 저장하세요")
if target.suffix != ".json":
raise ValueError("보고서는 새 .json 파일로 지정하세요")
if os.path.lexists(target):
raise ValueError("기존 보고서 보존: 새 --report 이름을 지정하세요")
report = inspect(calendar, folder, start_text, end_text)
rendered = json.dumps(report, ensure_ascii=False, indent=2, allow_nan=False) + "\n"
target.parent.mkdir(parents=True, exist_ok=True)
plain_path(target.parent)
try:
handle = target.open("x", encoding="utf-8", newline="\n")
except FileExistsError as error:
raise ValueError("기존 보고서 보존: 새 --report 이름을 지정하세요") from error
created_identity = None
try:
with handle:
info = os.fstat(handle.fileno())
created_identity = (info.st_dev, info.st_ino)
handle.write(rendered)
except OSError as error:
try:
current = target.lstat()
same_file = (current.st_dev, current.st_ino) == created_identity
if stat.S_ISREG(current.st_mode) and same_file:
target.unlink()
except FileNotFoundError:
pass
except OSError as cleanup_error:
raise OSError("저장과 새 보고서 정리에 실패했습니다. 출력 경로를 확인하세요") from cleanup_error
raise error
return report
def main():
parser = argparse.ArgumentParser(description="명시 달력과 일반 파일 이름의 수신 대조")
for option in ("calendar", "folder", "start", "end", "report"):
parser.add_argument("--" + option, required=True)
args = parser.parse_args()
try:
report = write_report(args.calendar, args.folder, args.start, args.end, args.report)
except (ValueError, OSError) as error:
print("오류: " + str(error), file=sys.stderr)
return 2
counts = report["summary"]
print(f"필수 {counts['expected_dates']} / 확인 {counts['received_dates']} / "
f"누락 {counts['missing_dates']} / 예상 밖 {counts['unexpected_files']} / "
f"이름 오류 {counts['invalid_filenames']} / 보류 {counts['held_entries']} / "
f"날짜 중복 {counts['duplicate_dates']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
read_calendar()는 달력 CSV만 읽고, inspect()는 검사 폴더의 파일 내용을 열지 않습니다. observed는 날짜별 파일 목록입니다. 목록이 없는 필수 날짜는 누락, 두 개 이상이면 날짜 중복, 정확히 한 개면 확인된 날짜에 들어갑니다. JSON에는 달력의 원래 행 ID와 논리 레코드 번호도 남습니다. CSV 필드를 문자열로 읽는 기본 동작은 Python csv 공식 문서에서 확인할 수 있습니다.
4 2일 누락과 6일 예상 밖을 확인합니다
터미널을 ZIP을 푼 폴더로 옮기고 다음 명령을 실행합니다. out은 검사 폴더의 바깥에 있으며 필요하면 새로 만들어집니다. -B는 파이썬의 바이트코드 캐시 파일을 만들지 않는 옵션입니다.
python -B code/check_received.py --calendar fixtures/calendar.csv --folder fixtures/received --start 2026-10-01 --end 2026-10-05 --report out/received.json
콘솔에는 다음 값이 나옵니다.
필수 3 / 확인 2 / 누락 1 / 예상 밖 1 / 이름 오류 0 / 보류 0 / 날짜 중복 0
out/received.json을 열고 다음 세 목록을 직접 읽습니다.
received_dates에는 2026-10-01과 2026-10-05가 있습니다missing_dates는["2026-10-02"]입니다unexpected_files에는daily_2026-10-06_A.csv와 이유outside_range가 있습니다
no_delivery_dates는 ["2026-10-03", "2026-10-04"]이고, all_expected_present는 false입니다. 이 값은 필수 날짜마다 일반 파일 하나가 있는지만 요약합니다. 파일이 비어 있어도 이름과 존재 조건을 만족하면 확인된 날짜로 세므로, 실제 업무에는 내용 검사와 전송 완료 확인이 별도로 필요합니다.
종료코드 0은 검사가 끝나 보고서를 저장했다는 뜻입니다. 누락이 발견돼도 0으로 종료합니다. 종료코드만 보고 모든 파일이 왔다고 판단하지 말고 JSON의 목록과 값을 읽으세요.
5 파일명 오류와 중복 날짜를 따로 읽습니다
filename_issues에는 기본 세 파일과 다음 두 반례가 있습니다. 잘못된 이름도 원문 그대로 보고서에 남습니다.
daily_2026-10-2_A.csv: 날짜의 일이 한 자리라pattern_mismatchdaily_2026-02-30_A.csv: 형식은 맞지만 없는 날짜라invalid_date
python -B code/check_received.py --calendar fixtures/calendar.csv --folder fixtures/filename_issues --start 2026-10-01 --end 2026-10-05 --report out/filename_issues.json
결과는 필수 3·확인 2·누락 1·예상 밖 1·이름 오류 2입니다. 이름 오류 두 파일을 2일 수신 증거로 쓰거나 예상 밖 날짜로 추정하지 않습니다. invalid_filenames의 name과 reason을 읽고 발신자가 의도한 날짜를 따로 확인하세요.
duplicate_files에는 1일의 A와 B 두 파일이 있습니다.
python -B code/check_received.py --calendar fixtures/calendar.csv --folder fixtures/duplicate_files --start 2026-10-01 --end 2026-10-05 --report out/duplicate_files.json
이 결과의 확인 날짜는 5일 한 개, 누락은 2일 한 개, 날짜 중복은 1일 한 개입니다. 중복 날짜 1일은 파일 자체가 없어 생긴 누락에 넣지 않습니다. duplicate_dates의 원래 파일명 두 개를 확인한 뒤 사용할 파일이나 수신 규칙을 정하세요.

6 폴더와 비수신일의 파일을 구별합니다
directories에는 daily_2026-10-02_A.csv라는 이름의 폴더가 있습니다. 그 안의 파일도 검사하지 않습니다.
python -B code/check_received.py --calendar fixtures/calendar.csv --folder fixtures/directories --start 2026-10-01 --end 2026-10-05 --report out/directories.json
2일은 여전히 누락 한 건이고 폴더는 held_entries에 kind=directory로 보류됩니다. 실제 심볼릭 링크도 같은 보류 목록에 kind=symlink로 기록하며 대상을 열지 않습니다. 시험 코드는 별도 임시 폴더에서 정상·끊어진 링크와 특수 파일도 확인합니다.
no_delivery_file에는 가상 비수신일인 3일의 일반 파일을 하나 더 넣었습니다.
python -B code/check_received.py --calendar fixtures/calendar.csv --folder fixtures/no_delivery_file --start 2026-10-01 --end 2026-10-05 --report out/no_delivery_file.json
예상 밖 파일은 3일의 no_delivery와 6일의 outside_range, 합계 두 개입니다. 누락은 계속 2일 한 개입니다. 비수신일에 온 파일을 다른 날짜의 빈자리를 채운 것으로 처리하지 않습니다.
7 잘못된 달력과 기존 보고서는 보존합니다
아래 네 명령은 차례대로 달력 ID 중복, 달력 날짜 중복, 불가능한 달력 날짜, 검사 기간 밖의 달력 날짜를 시험합니다. 각 명령은 종료코드 2이고 새 보고서가 없어야 합니다.
python -B code/check_received.py --calendar fixtures/duplicate_id.csv --folder fixtures/received --start 2026-10-01 --end 2026-10-05 --report out/rejected_duplicate_id.json
python -B code/check_received.py --calendar fixtures/duplicate_date.csv --folder fixtures/received --start 2026-10-01 --end 2026-10-05 --report out/rejected_duplicate_date.json
python -B code/check_received.py --calendar fixtures/impossible_date.csv --folder fixtures/received --start 2026-10-01 --end 2026-10-05 --report out/rejected_impossible_date.json
python -B code/check_received.py --calendar fixtures/outside_period.csv --folder fixtures/received --start 2026-10-01 --end 2026-10-05 --report out/rejected_outside_period.json
입력 조건을 위반하면 전체 대조를 중단합니다. 행을 버리거나 중복을 합친 부분 보고서를 정상 결과로 저장하지 않습니다. 반면 실제 폴더에서 발견한 이름 오류·중복·폴더·링크는 검토할 증거이므로 별도 목록을 가진 보고서를 저장합니다.
검사 폴더 안에 보고서를 지정하는 다음 명령도 거부합니다.
python -B code/check_received.py --calendar fixtures/calendar.csv --folder fixtures/received --start 2026-10-01 --end 2026-10-05 --report fixtures/received/report.json
앞에서 만든 보고서와 같은 이름으로 다시 실행하면 기존 파일을 그대로 두고 종료코드 2로 끝납니다.
python -B code/check_received.py --calendar fixtures/calendar.csv --folder fixtures/received --start 2026-10-01 --end 2026-10-05 --report out/received.json
재검사할 때는 --report out/received_again.json처럼 새 이름을 정하세요. Python 파일 열기 공식 문서의 배타적 생성 모드 x를 사용하므로 이미 있는 파일을 덮어쓰지 않습니다. 쓰기나 닫기 중 OSError가 나면, 연 파일 핸들의 장치·파일 식별값과 현재 출력 경로를 비교해 이번에 만든 같은 파일일 때만 정리합니다. 다른 파일로 교체된 경로를 삭제하지 않습니다. 정리 권한이나 저장 장치 자체에 문제가 있으면 오류와 출력 경로를 확인해야 합니다. 이 처리는 원자적 저장이나 갑작스러운 프로세스 종료까지 보장하지 않습니다.
8 실습 ZIP과 검산 범위를 확인합니다
ZIP에는 전체 코드, 시험 코드, 가상 달력과 폴더, 다섯 고정 기대 보고서, README가 들어 있습니다. out 결과와 expected의 같은 이름 JSON을 대조하면 됩니다. 다음 명령은 실제 업무 폴더 대신 새 임시 가상 자료에서 회귀시험을 실행합니다.
python -B -m unittest discover -s code -p test_check_received.py -v
시험이 정상 완료되면 Ran 39 tests와 OK를 확인할 수 있습니다. 시험 코드는 고정 기대값, 가상 입력의 경로와 SHA-256 보존, 기존 보고서 보존을 확인합니다. 부분 쓰기·닫기 오류에서는 이번에 만든 같은 새 보고서만 정리되는지 함께 검사합니다.
조회 중 다른 프로그램이 파일을 추가·교체·삭제하면 목록이 한 시점의 완전한 상태라는 보장이 없습니다. Path.iterdir 문서의 변경 주의사항도 순회 중 추가·삭제된 항목의 포함 여부를 보장하지 않습니다. 쓰는 중인 폴더를 피하고, 필요한 경우 발신 측의 완료 신호와 내용 검산 절차를 별도로 정하세요.
업무에 적용할 때는 먼저 누가 어떤 날짜에 파일을 보내기로 했는지 달력을 확정하고, 파일명 규칙과 날짜 중복 처리 기준을 합의하세요. 파일 이동이 필요한 경우에는 확장자별 폴더 정리 실습을, 일정 계산이 필요한 경우에는 WORKDAY와 NETWORKDAYS 날짜 검산을 함께 볼 수 있습니다.
월말 취합 전에는 all_expected_present와 함께 missing_dates·duplicate_dates·invalid_filenames·held_entries를 읽습니다. 누락·중복·이름 오류·보류 항목의 원인이 미해결이면 최종 취합 완료를 보류합니다. unexpected_files도 각 이유와 대상 파일을 확인해 이번 작업에 포함할지 정하세요. 필수 날짜에 파일이 하나씩 있으면 예상 밖 파일이나 이름 오류가 따로 있어도 all_expected_present는 true일 수 있습니다. 누락을 없애려고 합의된 yes를 임의로 no로 바꾸지 않습니다.
다음 단계는 자료의 목적에 따라 고릅니다. 같은 작업의 주문·기준정보 CSV를 짝지어야 하면 batch와 역할로 CSV 한 쌍 확인하기에서 선언 JSON과 모든 행의 batch를 확인합니다. 같은 구조의 Excel 파일을 한 표로 취합하려면 Power Query 폴더 파일 취합에서 원본 파일 목록·행수·금액을 대조합니다. 수신 대조의 JSON은 두 실습의 자동 입력 파일이 아니며, 날짜 파일명의 일괄ID를 CSV 본문의 batch로 자동 해석하지도 않습니다. 각 글의 입력 계약에 맞는 자료인지 별도로 확인하세요.