generated from ttipo/project-base
docs: 11단계 대용량 첨부 — 계약 4절 · 디자인 보드 다섯 · README #7
@@ -0,0 +1,459 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>메일 쓰기 · 대용량 첨부</title>
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<style>
|
||||
@font-face{font-family:'Pretendard';src:url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2-variations'),url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2');font-weight:45 920;font-style:normal;font-display:swap}
|
||||
body{margin:0}
|
||||
a{color:#0E7C86}a:hover{color:#0A5F67}
|
||||
:focus-visible{outline:2px solid #0E7C86;outline-offset:2px}
|
||||
.primary{background:#0E7C86;color:#FFFFFF}
|
||||
.primary:hover{background:#0A5F67}
|
||||
.ghost{background:transparent;color:#4A5363}
|
||||
.ghost:hover{background:#F2F4F7;color:#171C26}
|
||||
.icon-btn{background:transparent;color:#4A5363}
|
||||
.icon-btn:hover{background:#F2F4F7;color:#171C26}
|
||||
.card-btn{background:#FFFFFF;color:#0A5F67;border:1px solid #D0D5DD}
|
||||
.card-btn:hover{background:#F2F4F7}
|
||||
.warn-btn{background:#FFFFFF;color:#7A5200;border:1px solid #F0D9A8}
|
||||
.warn-btn:hover{background:#FFF1D6}
|
||||
.danger-btn{background:#FFFFFF;color:#B42318;border:1px solid #FBD7D3}
|
||||
.danger-btn:hover{background:#FEF3F2}
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="width: 1440px; height: 2360px; box-sizing: border-box; padding: 48px; display: flex; flex-direction: column; gap: 28px; background: #F6F7F9; color: #171C26; font-family: Pretendard, -apple-system, 'Apple SD Gothic Neo', 'Malgun Gothic', system-ui, sans-serif; font-size: 13px; line-height: 1.5; word-break: keep-all; overflow-wrap: anywhere">
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 6px; max-width: 1060px">
|
||||
<h1 style="margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.02em">메일 쓰기 · 대용량 첨부</h1>
|
||||
<p style="margin: 0; font-size: 13.5px; color: #4A5363">20MB 가 넘는 파일과, 메일에 담은 파일의 합계를 25MB 넘게 만드는 파일은 링크로 보냅니다. 파일은 회사 서버에 올라가고, 보낼 때 서버가 본문에 내려받기 안내를 넣습니다. 받는 사람은 보낸 날부터 30일 동안 내려받습니다. 큰 파일은 조각으로 나눠 올려 끊겨도 올린 만큼은 남습니다. 초안 — 사용자 확인 전.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 800px minmax(0, 1fr); gap: 20px; align-items: start">
|
||||
|
||||
<section aria-label="쓰기 화면 아래쪽 — 일반 첨부와 링크 파일이 섞인 때" style="display: flex; flex-direction: column; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px; overflow: hidden">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; padding: 12px 32px; border-bottom: 1px solid #EEF0F3; background: #FBFBFC">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">쓰기 화면 — 첨부 칸 · 보내기 줄</h2>
|
||||
<span style="font-size: 12px; color: #667085">콘텐츠 열 800 · 32px 선 그대로 · 위쪽(받는 사람 · 제목 · 툴바)은 줄였습니다</span>
|
||||
</div>
|
||||
|
||||
<div style="padding: 14px 32px 18px; border-bottom: 1px solid #EEF0F3; font-family: 'Malgun Gothic', '맑은 고딕', 'Apple SD Gothic Neo', sans-serif; font-size: 14px; line-height: 1.75; color: #1F2733">
|
||||
<p style="margin: 0">김민수 과장님, 요청하신 설계도와 현장 영상 보내드립니다.</p>
|
||||
<p style="margin: 0">파일이 커서 링크로 보내니 메일 아래의 주소로 내려받아 주세요.</p>
|
||||
<p style="margin: 0"><br></p>
|
||||
<p style="margin: 0; color: #4A5363">김아라 · 인프라팀 · ARA SYSTEM</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; padding: 10px 32px 14px">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px">
|
||||
<button type="button" class="ghost" style="display: inline-flex; align-items: center; gap: 6px; min-height: 44px; margin-left: -6px; padding: 0 10px 0 6px; border: 0; border-radius: 8px; color: #344054; font: inherit; font-weight: 500; cursor: pointer">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>
|
||||
<span>파일 첨부</span>
|
||||
</button>
|
||||
<span style="font-size: 12px; color: #667085">파일당 최대 20MB · 합계 25MB · 최대 20개 · 더 큰 파일은 링크로 보냅니다 (파일당 2GB · 10개까지)</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; margin-top: 2px">
|
||||
<h3 style="margin: 0; font-size: 12.5px; font-weight: 600; color: #344054">메일에 담는 파일 2개</h3>
|
||||
<span style="font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">합계 18.1 MB / 25 MB</span>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px">
|
||||
<li style="position: relative; display: flex; align-items: center; gap: 10px; padding: 2px 2px 2px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">견적서_11월.xlsx</span>
|
||||
<span style="flex: 0 0 auto; font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">52 KB</span>
|
||||
<button type="button" class="icon-btn" aria-label="견적서_11월.xlsx 링크로 보내기" aria-describedby="lf-tip" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; background: #F2F4F7; cursor: pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg></button>
|
||||
<button type="button" class="icon-btn" aria-label="견적서_11월.xlsx 첨부 취소" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
<span id="lf-tip" role="tooltip" style="position: absolute; right: 18px; bottom: calc(100% + 4px); z-index: 2; padding: 4px 8px; border-radius: 6px; background: #171C26; color: #FFFFFF; font-size: 12px; white-space: nowrap">링크로 보내기</span>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 10px; padding: 2px 2px 2px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">현장사진_요약.pdf</span>
|
||||
<span style="flex: 0 0 auto; font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">18 MB</span>
|
||||
<button type="button" class="icon-btn" aria-label="현장사진_요약.pdf 링크로 보내기" title="링크로 보내기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg></button>
|
||||
<button type="button" class="icon-btn" aria-label="현장사진_요약.pdf 첨부 취소" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; margin-top: 6px">
|
||||
<h3 style="margin: 0; font-size: 12.5px; font-weight: 600; color: #344054">링크로 보내는 파일 3개</h3>
|
||||
<span style="font-size: 12px; color: #667085">받는 사람은 보낸 날부터 30일 동안 내려받습니다 — 오늘 보내면 11월 29일(일)까지</span>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px">
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">설계도_v3.dwg</span>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 올리기 멈춤" title="멈춤" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg></button>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; padding: 0 10px 0 26px; font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">
|
||||
<span style="flex: 1 1 auto">올리는 중 · 420 MB / 1.2 GB</span>
|
||||
<span>약 3분 남음</span>
|
||||
</div>
|
||||
<div role="progressbar" aria-label="설계도_v3.dwg 올리는 중" aria-valuemin="0" aria-valuemax="100" aria-valuenow="35" style="height: 3px; margin: 5px 10px 0 26px; border-radius: 2px; background: #E4E7EC"><div style="width: 35%; height: 3px; border-radius: 2px; background: #0E7C86"></div></div>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">현장영상_1029.mp4</span>
|
||||
<button type="button" class="icon-btn" aria-label="현장영상_1029.mp4 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="padding: 0 10px 0 26px; font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">612 MB · 보안 검사 중 — 끝나지 않아도 보낼 수 있습니다</div>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">회의자료_전체.pptx</span>
|
||||
<button type="button" class="icon-btn" aria-label="회의자료_전체.pptx 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="padding: 0 10px 0 26px; font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">9.4 MB</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p role="status" style="display: flex; align-items: flex-start; gap: 6px; margin: 0; font-size: 12.5px; color: #4A5363">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto; margin-top: 2px"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
<span>‘회의자료_전체.pptx’(9.4 MB)는 메일에 담으면 합계가 25MB 를 넘어 링크로 보냅니다.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 32px; border-top: 1px solid #E6E8EC">
|
||||
<div style="display: inline-flex; align-items: stretch; border-radius: 8px; overflow: hidden">
|
||||
<button type="submit" aria-disabled="true" aria-describedby="lf-send-why" style="display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 16px 0 18px; border: 0; background: #D0D5DD; color: #FFFFFF; font: inherit; font-size: 13.5px; font-weight: 600; cursor: not-allowed"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg><span>보내기</span></button>
|
||||
<span aria-hidden="true" style="width: 1px; background: rgba(255, 255, 255, 0.6)"></span>
|
||||
<button type="button" aria-disabled="true" aria-label="보내기 방법 더 보기" aria-describedby="lf-send-why" style="display: inline-flex; align-items: center; justify-content: center; width: 44px; min-height: 44px; border: 0; background: #D0D5DD; color: #FFFFFF; cursor: not-allowed"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg></button>
|
||||
</div>
|
||||
<button type="button" class="ghost" style="min-height: 44px; padding: 0 14px; border: 0; border-radius: 8px; color: #344054; font: inherit; font-weight: 500; cursor: pointer">임시 저장</button>
|
||||
<p id="lf-send-why" style="display: flex; align-items: center; gap: 6px; margin: 0; font-size: 12.5px; color: #4A5363">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
<span>파일 1개를 올리는 중입니다 — 다 올리면 보낼 수 있습니다.</span>
|
||||
</p>
|
||||
<span style="margin-left: auto; font-size: 12px; color: #667085">보내는 사람 김아라 <ara.kim@arasystem.it.kr></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 20px">
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">링크로 바뀌는 때</h2>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>파일 하나가 20MB 를 넘으면 그 파일은 링크로 보냅니다.</li>
|
||||
<li>메일에 담은 파일의 합계가 25MB 를 넘게 되면 그 파일부터 링크로 보냅니다(고른 차례대로). 그때 칸 아래에 한 줄로 알립니다(왼쪽 ⓘ).</li>
|
||||
<li>메일에 담지 못하는 형식(동영상 mp4 · mov, CAD dwg · dxf, 디자인 psd · ai, 7z)은 크기와 상관없이 링크로 보냅니다.</li>
|
||||
<li>메일에 담은 파일도 링크 단추(「링크로 보내기」)로 바꿀 수 있습니다 — 받는 사람이 많거나 받는 서버가 큰 메일을 막을 때. 단추는 이 쓰기 화면에서 고른 파일에만 있습니다 — 전달 원본 · 저장본에서 이어 온 첨부에는 없습니다(처음 판).</li>
|
||||
<li>받는 사람이 사내 직원이어도 같은 규칙입니다.</li>
|
||||
</ul>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">묻는 창은 띄우지 않습니다(국내 메일은 모두 저절로 바꾸고, 칸 안의 글로 충분합니다). 링크 파일을 메일에 담는 쪽으로 되돌리는 단추는 처음 판에 없습니다 — 빼고 다시 고르면 한도 안에서 메일에 담깁니다.</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">링크 파일의 한도 · 모양</h2>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>파일당 2GB · 메일 한 통에 10개 · 직원마다 동시에 보관 10GB(올리는 중 · 임시 저장 포함). 모두 운영 설정입니다.</li>
|
||||
<li>칸 안의 두 묶음 — 「메일에 담는 파일」 · 「링크로 보내는 파일」. 링크 파일만 있거나 일반 첨부만 있으면 묶음 이름은 그 하나만.</li>
|
||||
<li>링크 파일은 아이콘이 링크 모양(청록)이고 줄이 둘입니다 — 이름 · 단추 / 크기 · 상태(12px). 뱃지 · 알약 없이 글자만.</li>
|
||||
<li>✕ 는 「빼기」 — 올리던 것을 멈추고 서버에 올린 조각도 지웁니다.</li>
|
||||
</ul>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">본문 편집기에는 내려받기 안내가 보이지 않습니다 — 보낼 때 서버가 내 글 · 서명 아래, 인용 위에 넣습니다(「본문 덩이」 보드).</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; align-items: start">
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">올리는 중 · 멈춤</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">16MB 조각으로 올립니다 · 남은 시간은 최근 속도로 셉니다</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px">
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">설계도_v3.dwg</span>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 올리기 멈춤" title="멈춤" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg></button>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; padding: 0 10px 0 26px; font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">
|
||||
<span style="flex: 1 1 auto">올리는 중 · 420 MB / 1.2 GB</span>
|
||||
<span>약 3분 남음</span>
|
||||
</div>
|
||||
<div aria-hidden="true" style="height: 3px; margin: 5px 10px 0 26px; border-radius: 2px; background: #E4E7EC"><div style="width: 35%; height: 3px; border-radius: 2px; background: #0E7C86"></div></div>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">설계도_v3.dwg</span>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 이어 올리기" title="이어 올리기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="6 4 20 12 6 20 6 4"></polygon></svg></button>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="padding: 0 10px 0 26px; font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">멈춤 · 420 MB / 1.2 GB 올림</div>
|
||||
<div aria-hidden="true" style="height: 3px; margin: 5px 10px 0 26px; border-radius: 2px; background: #E4E7EC"><div style="width: 35%; height: 3px; border-radius: 2px; background: #98A2B3"></div></div>
|
||||
</li>
|
||||
</ul>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">[멈춤]은 그 파일만 멈추고 [이어 올리기]로 다시 올립니다 — 다른 파일은 계속 올립니다. 처음 몇 초는 남은 시간 대신 "시간 계산 중". 멈춘 파일이 있으면 보내기는 막힌 채입니다. 진행 글은 화면 낭독기에 10% 마다 알립니다.</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">잠깐 끊김 — 저절로 다시</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">망이 끊기거나 PC 가 잠들었다 깨었을 때</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px">
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">설계도_v3.dwg</span>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 올리기 멈춤" title="멈춤" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg></button>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; padding: 0 10px 0 26px; font-size: 12px; color: #7A5200; font-variant-numeric: tabular-nums">
|
||||
<span style="flex: 1 1 auto">연결이 끊겨 다시 잇는 중… · 420 MB / 1.2 GB 올림</span>
|
||||
</div>
|
||||
<div aria-hidden="true" style="height: 3px; margin: 5px 10px 0 26px; border-radius: 2px; background: #E4E7EC"><div style="width: 35%; height: 3px; border-radius: 2px; background: #0E7C86"></div></div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>2 · 5 · 15 · 30초 간격으로 저절로 다시 잇습니다(실시간 알림의 다시 잇기와 같은 결). 누를 것이 없습니다.</li>
|
||||
<li>서버에 받은 조각은 그대로 — 받지 못한 조각부터 다시 보냅니다. 이어지면 글이 "올리는 중"으로 돌아갑니다.</li>
|
||||
<li>로그인이 끝났으면 세션을 한 번 갱신하고 같은 조각을 다시 보냅니다(지금 요청 규칙). 갱신도 실패하면 세션 만료 창.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">오래 끊김 — [다시 시도]</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">저절로 다시 잇기를 1분 넘게 하지 못했을 때 · 올린 만큼은 남습니다</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px">
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #B42318">설계도_v3.dwg</span>
|
||||
<button type="button" class="danger-btn" style="flex: 0 0 auto; min-height: 40px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">다시 시도</button>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; color: #B42318; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p role="alert" style="margin: 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #B42318">올리지 못하고 멈췄습니다 — 올린 420 MB 는 남아 있습니다.</p>
|
||||
<div aria-hidden="true" style="height: 3px; margin: 5px 10px 0 26px; border-radius: 2px; background: #FBD7D3"><div style="width: 35%; height: 3px; border-radius: 2px; background: #B42318"></div></div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>[다시 시도]는 올린 곳부터 이어서 올립니다. 서버는 마지막 조각을 받은 뒤 24시간 기다립니다 — 그 뒤에는 처음부터 다시 올립니다("처음부터 다시 올립니다" 한 줄).</li>
|
||||
<li>서버가 조각을 거절하면(받은 내용이 깨짐) 그 조각만 다시 보냅니다 — 사용자에게는 보이지 않습니다. 세 번 거듭되면 이 모양으로 멈춥니다.</li>
|
||||
<li>알림은 띄우지 않습니다 — 칸 안의 빨간 줄과 보내기 줄의 까닭으로 충분합니다.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; align-items: start">
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">다 올린 뒤 — 보안 검사</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">서버가 올리기를 마치는 대로 검사합니다(ClamAV)</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px">
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">현장영상_1029.mp4</span>
|
||||
<button type="button" class="icon-btn" aria-label="현장영상_1029.mp4 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="padding: 0 10px 0 26px; font-size: 12px; color: #667085">612 MB · 보안 검사 중 — 끝나지 않아도 보낼 수 있습니다</div>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">설계도_v3.dwg</span>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="padding: 0 10px 0 26px; font-size: 12px; color: #667085">1.2 GB</div>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">협력사_자료모음.7z</span>
|
||||
<button type="button" class="icon-btn" aria-label="협력사_자료모음.7z 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="padding: 0 10px 0 26px; font-size: 12px; color: #667085">860 MB · 검사하지 못함 — 암호가 걸린 압축 파일</div>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #B42318">견적_요청서.zip</span>
|
||||
<button type="button" class="icon-btn" aria-label="견적_요청서.zip 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; color: #B42318; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p role="alert" style="margin: 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #B42318">위험한 내용이 발견되어 보낼 수 없습니다. 이 파일을 빼 주세요.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">검사가 끝나기 전에 보내도 됩니다 — 받는 사람은 검사가 끝난 뒤 받습니다. 검사가 끝난 깨끗한 파일은 크기만 남습니다. 암호 압축처럼 검사하지 못하는 파일은 막지 않고 글로만 알립니다. 위험이면 보낼 수 없고, 보낸 뒤에 위험이 나오면 그 파일을 막고 알립니다(설정 › 대용량 첨부).</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">한도를 넘을 때 — 올리지 않는다</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">고르는 순간 먼저 검사합니다(지금 첨부 사전 검사와 같은 결)</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px">
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #B42318">전체백업_2026.zip</span>
|
||||
<span style="flex: 0 0 auto; font-size: 12px; color: #B42318; font-variant-numeric: tabular-nums">2.4 GB</span>
|
||||
<button type="button" class="icon-btn" aria-label="전체백업_2026.zip 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; color: #B42318; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p role="alert" style="margin: 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #B42318">파일 하나는 2GB 까지 보낼 수 있습니다.</p>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #B42318">회의녹화_11.mp4</span>
|
||||
<span style="flex: 0 0 auto; font-size: 12px; color: #B42318; font-variant-numeric: tabular-nums">380 MB</span>
|
||||
<button type="button" class="icon-btn" aria-label="회의녹화_11.mp4 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; color: #B42318; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p role="alert" style="margin: 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #B42318">링크로 보내는 파일은 메일 한 통에 10개까지입니다.</p>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #B42318">시공영상_전체.mov</span>
|
||||
<span style="flex: 0 0 auto; font-size: 12px; color: #B42318; font-variant-numeric: tabular-nums">1.9 GB</span>
|
||||
<button type="button" class="icon-btn" aria-label="시공영상_전체.mov 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; color: #B42318; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p role="alert" style="margin: 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #B42318">대용량 보관 한도(10GB)를 넘어 올릴 수 없습니다. 보내지 않은 파일을 지우거나 링크를 끊으면 바로 공간이 생깁니다. <a href="SettingsLargeFiles.dc.html" style="color: #B42318; font-weight: 600">대용량 첨부 보기</a></p>
|
||||
</li>
|
||||
</ul>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; padding: 8px 10px; border: 1px dashed #D0D5DD; border-radius: 8px">
|
||||
<span style="font-size: 12.5px; font-weight: 600; color: #344054">링크로 보내는 파일 2개</span>
|
||||
<span style="font-size: 12px; color: #667085">받는 사람은 보낸 날부터 30일 …</span>
|
||||
<span style="margin-left: auto; font-size: 12px; font-weight: 600; color: #7A5200; font-variant-numeric: tabular-nums">보관 8.4 / 10 GB</span>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">넘는 파일은 빨간 줄로 남고 보내기를 막지 않습니다(그 파일만 빠집니다 — 지금 첨부와 같다). 보관 사용량은 80%를 넘을 때만 링크 묶음 머리 오른쪽에 보입니다(점선 상자). 서버 공간이 모자라면 같은 모양으로 "지금은 서버에 공간이 모자라 올릴 수 없습니다. 잠시 후 다시 시도해 주세요."</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">막힌 형식</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">허용 목록에 있는 형식만 — 실행 파일 · 디스크 이미지는 막습니다</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px">
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #B42318">설치파일_v2.exe</span>
|
||||
<span style="flex: 0 0 auto; font-size: 12px; color: #B42318; font-variant-numeric: tabular-nums">48 MB</span>
|
||||
<button type="button" class="icon-btn" aria-label="설치파일_v2.exe 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; color: #B42318; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p role="alert" style="margin: 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #B42318">허용되지 않는 파일 형식입니다. 파일을 확인해 주세요.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<dl style="margin: 0; display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 6px 10px; font-size: 12.5px">
|
||||
<dt style="color: #667085">메일에 담기</dt><dd style="margin: 0; color: #344054">pdf · doc · docx · xls · xlsx · ppt · pptx · hwp · hwpx · txt · csv · png · jpg · jpeg · gif · webp · zip (지금 그대로)</dd>
|
||||
<dt style="color: #667085">링크로 더</dt><dd style="margin: 0; color: #344054">mp4 · mov · dwg · dxf · psd · ai · 7z</dd>
|
||||
<dt style="color: #667085">막음</dt><dd style="margin: 0; color: #344054">exe · msi · bat · cmd · js · vbs · scr 같은 실행 파일, iso · img · vhd 같은 디스크 이미지, 그 밖의 목록 밖 형식</dd>
|
||||
</dl>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">파일 고르기 창도 두 목록을 합친 형식만 보여 줍니다. 서버도 이름의 형식을 다시 봅니다. 받는 쪽은 형식과 상관없이 늘 "내려받기"로만 받습니다 — 브라우저가 열어 실행하지 않습니다. 칸 아래 문구는 사전의 FILE_001 그대로입니다.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; align-items: start">
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">새로고침 · 창을 닫은 뒤 — 저장본을 다시 열면</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">브라우저는 고른 파일을 스스로 다시 읽지 못합니다</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px">
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #F0D9A8; border-radius: 8px; background: #FFF7E6">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#7A5200" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">현장영상_1029.mp4</span>
|
||||
<button type="button" class="warn-btn" style="flex: 0 0 auto; min-height: 40px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">파일 고르기</button>
|
||||
<button type="button" class="icon-btn" aria-label="현장영상_1029.mp4 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p style="margin: 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #7A5200">올리다 멈춘 파일 — 같은 파일을 다시 고르면 이어서 올립니다 · 420 MB / 1.2 GB 올림</p>
|
||||
<div aria-hidden="true" style="height: 3px; margin: 5px 10px 0 26px; border-radius: 2px; background: #F0D9A8"><div style="width: 35%; height: 3px; border-radius: 2px; background: #B7791F"></div></div>
|
||||
<p role="alert" style="margin: 4px 0 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #B42318">고른 파일이 멈춘 파일과 다릅니다(이름 · 크기). 같은 파일을 골라 주세요.</p>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0A5F67" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">설계도_v3.dwg</span>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<div style="padding: 0 10px 0 26px; font-size: 12px; color: #667085">1.2 GB · 이 임시 저장본에 11월 29일까지 남습니다</div>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #B42318">회의녹화_09.mp4</span>
|
||||
<button type="button" class="icon-btn" aria-label="회의녹화_09.mp4 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; color: #B42318; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p role="alert" style="margin: 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #B42318">기간이 지나 지운 파일입니다 — 빼고 다시 올려 주세요.</p>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 8px 10px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #B42318">시공도면_v4.dwg</span>
|
||||
<button type="button" class="icon-btn" aria-label="시공도면_v4.dwg 빼기" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; color: #B42318; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p role="alert" style="margin: 0; padding: 0 10px 0 26px; font-size: 12.5px; color: #B42318">설정 › 대용량 첨부에서 지운 파일입니다 — 빼고 다시 올려 주세요.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">그래서 같은 파일을 한 번 더 골라야 이어서 올립니다(이름 · 크기 · 고친 시각으로 맞춰 봅니다 — 서버는 마지막 조각 뒤 24시간 기다립니다). 다 올린 파일은 임시 저장본에 붙어 올린 날부터 30일 동안 남고, 지나거나 설정 › 대용량 첨부에서 지우면 빨간 줄입니다(글만 다르다). 네이버웍스 모바일처럼 저장하면 큰 파일이 빠지는 일은 없습니다.</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">다 올릴 때까지 보내기를 막는다</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">꺼진 단추는 aria-disabled — 까닭을 단추 옆 글로</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 12px; border: 1px solid #EEF0F3; border-radius: 10px">
|
||||
<div style="display: inline-flex; align-items: stretch; border-radius: 8px; overflow: hidden">
|
||||
<button type="submit" aria-disabled="true" aria-describedby="lf-send-why-2" style="display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 16px 0 18px; border: 0; background: #D0D5DD; color: #FFFFFF; font: inherit; font-size: 13.5px; font-weight: 600; cursor: not-allowed"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg><span>보내기</span></button>
|
||||
<span aria-hidden="true" style="width: 1px; background: rgba(255, 255, 255, 0.6)"></span>
|
||||
<button type="button" aria-disabled="true" aria-label="보내기 방법 더 보기" aria-describedby="lf-send-why-2" style="display: inline-flex; align-items: center; justify-content: center; width: 44px; min-height: 44px; border: 0; background: #D0D5DD; color: #FFFFFF; cursor: not-allowed"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg></button>
|
||||
</div>
|
||||
<button type="button" class="ghost" style="min-height: 44px; padding: 0 14px; border: 0; border-radius: 8px; color: #344054; font: inherit; font-weight: 500; cursor: pointer">임시 저장</button>
|
||||
<p id="lf-send-why-2" style="flex: 1 1 100%; margin: 0; font-size: 12.5px; color: #4A5363">파일 2개를 올리는 중입니다 — 다 올리면 보낼 수 있습니다.</p>
|
||||
</div>
|
||||
<dl style="margin: 0; display: flex; flex-direction: column; gap: 8px; font-size: 12.5px">
|
||||
<div style="display: flex; flex-direction: column; gap: 1px"><dt style="color: #667085">올리는 중 · 다시 잇는 중</dt><dd style="margin: 0; color: #344054">"파일 2개를 올리는 중입니다 — 다 올리면 보낼 수 있습니다."</dd></div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1px"><dt style="color: #667085">멈춤 · 오래 끊김 · 새로고침 뒤 멈춘 파일</dt><dd style="margin: 0; color: #344054">"다 올리지 못한 파일이 있습니다 — 이어 올리거나 빼 주세요."</dd></div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1px"><dt style="color: #667085">위험 · 지운 파일(기간 지남 · 설정에서 지움)</dt><dd style="margin: 0; color: #344054">"보낼 수 없는 파일이 있습니다 — 빨간 줄의 파일을 빼 주세요."</dd></div>
|
||||
</dl>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">[보내기]와 ▾ 는 초점을 받습니다. 누르거나 Ctrl+Enter 를 치면 보내지 않고 까닭 글을 화면 낭독기에 읽어 줍니다(role=status). 「예약 발송…」도 같은 까닭으로 막힙니다. 임시 저장은 그대로 됩니다 — 올리는 중인 파일도 저장본에 남습니다(왼쪽 카드).</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">나가기 확인 — 올리는 중인 파일</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">앱 안에서 다른 화면으로 갈 때 · 쓰기 화면 닫기(✕)</p>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding: 22px 16px; border-radius: 10px; background: rgba(16, 24, 28, 0.08)">
|
||||
<div role="alertdialog" aria-modal="true" aria-labelledby="lf-leave-title" aria-describedby="lf-leave-message" style="width: 100%; max-width: 360px; box-sizing: border-box; padding: 20px 20px 16px; border: 1px solid #E6E8EC; border-radius: 12px; background: #FFFFFF; box-shadow: 0 14px 36px rgba(16, 24, 28, 0.14); display: flex; flex-direction: column; gap: 8px">
|
||||
<h3 id="lf-leave-title" style="margin: 0; font-size: 15px; font-weight: 700">올리는 중인 파일이 있습니다</h3>
|
||||
<p id="lf-leave-message" style="margin: 0; color: #4A5363">이 화면을 나가면 올리기가 멈춥니다. 임시보관함에서 이 메일을 다시 열고 같은 파일을 고르면 이어서 올립니다.</p>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px">
|
||||
<button type="button" class="primary" style="min-height: 44px; padding: 0 16px; border: 0; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer; outline: 2px solid #0E7C86; outline-offset: 2px">계속 쓰기</button>
|
||||
<button type="button" class="ghost" style="min-height: 44px; padding: 0 14px; border: 0; border-radius: 8px; font: inherit; font-weight: 500; cursor: pointer">나가기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">처음 초점은 [계속 쓰기] · Esc 도 같습니다(지금 나가기 확인과 같은 모양). 떠나기 전 임시 저장은 지금처럼 먼저 하고, 올리는 중인 파일은 저장본에 "올리다 멈춘 파일"로 남깁니다. 탭을 닫거나 새로 고치면 브라우저가 묻습니다. 다 올린 뒤에는 이 확인이 없습니다.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":1440,"height":2360}}'>
|
||||
class Component extends DCLogic {
|
||||
renderVals() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,213 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>본문 덩이 — 바깥 메일 프로그램에서</title>
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<style>
|
||||
@font-face{font-family:'Pretendard';src:url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2-variations'),url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2');font-weight:45 920;font-style:normal;font-display:swap}
|
||||
body{margin:0}
|
||||
a{color:#0E7C86}a:hover{color:#0A5F67}
|
||||
:focus-visible{outline:2px solid #0E7C86;outline-offset:2px}
|
||||
.ghost{background:transparent;color:#4A5363}
|
||||
.ghost:hover{background:#F2F4F7;color:#171C26}
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="width: 1440px; height: 2020px; box-sizing: border-box; padding: 48px; display: flex; flex-direction: column; gap: 28px; background: #F6F7F9; color: #171C26; font-family: Pretendard, -apple-system, 'Apple SD Gothic Neo', 'Malgun Gothic', system-ui, sans-serif; font-size: 13px; line-height: 1.5; word-break: keep-all; overflow-wrap: anywhere">
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 6px; max-width: 1060px">
|
||||
<h1 style="margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.02em">본문 덩이 — 바깥 메일 프로그램에서</h1>
|
||||
<p style="margin: 0; font-size: 13.5px; color: #4A5363">링크 파일이 있는 메일은 보낼 때 서버가 본문에 내려받기 안내(덩이)를 넣습니다. 자리는 내 글 · 서명 아래, 인용 위입니다. 서식 본문(HTML)과 텍스트 대체 본문에 같은 내용이 들어갑니다. 우리 웹메일 안에서는 링크 주인이 보낸 메일이면 덩이를 접고 첨부 칸에 첨부처럼 보입니다(「읽기 화면 · 대용량 첨부」 보드). 초안 — 사용자 확인 전.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 820px minmax(0, 1fr); gap: 20px; align-items: start">
|
||||
|
||||
<section aria-label="바깥 메일 프로그램에서 받은 답장 메일" style="display: flex; flex-direction: column; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px; overflow: hidden">
|
||||
<div style="display: flex; align-items: center; gap: 10px; min-height: 40px; padding: 0 20px; background: #EEF0F3; color: #4A5363; font-size: 12px">
|
||||
<span style="font-weight: 600">바깥 메일 프로그램</span>
|
||||
<span>(예: Gmail · Outlook · 네이버 메일 — 받는 사람 김민수, 거래처)</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 4px; padding: 18px 28px 14px; border-bottom: 1px solid #EEF0F3; font-family: system-ui, -apple-system, 'Segoe UI', 'Malgun Gothic', sans-serif">
|
||||
<h2 style="margin: 0 0 6px; font-size: 18px; font-weight: 600; color: #1F1F1F">Re: 11월 현장 점검 자료 요청</h2>
|
||||
<span style="font-size: 13px; color: #1F1F1F"><strong style="font-weight: 600">김아라</strong> <ara.kim@arasystem.it.kr></span>
|
||||
<span style="font-size: 12.5px; color: #5F6368">받는 사람: 김민수 <minsu.kim@hanbit-partners.co.kr> · 2026. 10. 30. 오후 2:15</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; padding: 20px 28px 28px; font-family: 'Malgun Gothic', '맑은 고딕', 'Apple SD Gothic Neo', sans-serif; font-size: 14px; line-height: 1.75; color: #1F2733">
|
||||
<p style="margin: 0">김민수 과장님, 안녕하세요.</p>
|
||||
<p style="margin: 0"><br></p>
|
||||
<p style="margin: 0">요청하신 설계도와 현장 영상 보내드립니다.</p>
|
||||
<p style="margin: 0">파일이 커서 아래 주소로 내려받으실 수 있습니다.</p>
|
||||
<p style="margin: 0"><br></p>
|
||||
<p style="margin: 0">김아라 · 인프라팀 · ARA SYSTEM</p>
|
||||
<p style="margin: 0">010-1234-5678</p>
|
||||
|
||||
<table aria-label="대용량 첨부 안내 — 서버가 넣은 덩이" style="margin: 16px 0 0; border-collapse: collapse; border: 1px solid #D0D5DD; background: #FFFFFF">
|
||||
<tbody>
|
||||
<tr><td colspan="2" style="padding: 12px 16px 2px; font-size: 14px; font-weight: 700; color: #171C26">대용량 첨부 2개 (합계 1.8 GB)</td></tr>
|
||||
<tr><td colspan="2" style="padding: 0 16px 8px; font-size: 13px; color: #344054">2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간)</td></tr>
|
||||
<tr><td style="padding: 0 0 0 16px; font-size: 13px; color: #171C26">설계도_v3.dwg</td><td style="padding: 0 16px 0 16px; font-size: 13px; color: #667085; text-align: right; white-space: nowrap">1.2 GB</td></tr>
|
||||
<tr><td style="padding: 0 0 0 16px; font-size: 13px; color: #171C26">현장영상_1029.mp4</td><td style="padding: 0 16px 0 16px; font-size: 13px; color: #667085; text-align: right; white-space: nowrap">612 MB</td></tr>
|
||||
<tr><td colspan="2" style="padding: 10px 16px 0; font-size: 13px; color: #171C26">내려받기 <a href="SharePage.dc.html" style="color: #0E7C86">https://files.arasystem.it.kr/s/Qx7mK2vR9pL4wT8nB3cF6hJ1dS5gA0eY2uI7oP9kZ3x</a></td></tr>
|
||||
<tr><td colspan="2" style="padding: 6px 16px 0; font-size: 12px; color: #667085">내려받을 때 로그인이나 비밀번호를 묻지 않습니다.</td></tr>
|
||||
<tr><td colspan="2" style="padding: 2px 16px 12px; font-size: 12px; color: #667085">Large files: download by Nov 29, 2026 (KST) — no login or password needed.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<blockquote style="margin: 24px 0 0; padding: 2px 0 2px 14px; border-left: 2px solid #D0D5DD; color: #4A5363">
|
||||
<div style="margin: 0 0 12px; font-size: 12.5px; line-height: 1.6">
|
||||
<p style="margin: 0; font-weight: 600">원본 메일</p>
|
||||
<table style="border-collapse: collapse; font-size: 12.5px; line-height: 1.6">
|
||||
<tbody>
|
||||
<tr><th scope="row" style="padding: 0 12px 0 0; text-align: left; vertical-align: top; font-weight: 400; color: #667085; white-space: nowrap">보낸 사람</th><td style="padding: 0">김민수 <minsu.kim@hanbit-partners.co.kr></td></tr>
|
||||
<tr><th scope="row" style="padding: 0 12px 0 0; text-align: left; vertical-align: top; font-weight: 400; color: #667085; white-space: nowrap">날짜</th><td style="padding: 0">2026. 10. 29. (목) 오후 5:02</td></tr>
|
||||
<tr><th scope="row" style="padding: 0 12px 0 0; text-align: left; vertical-align: top; font-weight: 400; color: #667085; white-space: nowrap">제목</th><td style="padding: 0">11월 현장 점검 자료 요청</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p style="margin: 0">김아라 매니저님, 11월 현장 점검 자료(설계도 · 현장 영상)를 보내 주실 수 있을까요?</p>
|
||||
<p style="margin: 0">용량이 크면 나눠서 보내 주셔도 됩니다.</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">덩이에 들어가는 것 (위에서부터)</h2>
|
||||
<ol style="margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li><strong style="font-weight: 600">제목 줄</strong> — "대용량 첨부 2개 (합계 1.8 GB)" 14px 굵게</li>
|
||||
<li><strong style="font-weight: 600">기간 줄</strong> — "2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간)". 보낸 때(예약 발송은 보낼 시각)부터 30일째 날 23:59:59 까지, 받는 사람 모두 같은 날</li>
|
||||
<li><strong style="font-weight: 600">파일 줄</strong> — 이름 · 크기(회색), 파일마다 한 줄. 이름은 링크가 아닙니다 — 메일 한 통에 링크가 하나라 모두 같은 화면으로 갑니다</li>
|
||||
<li><strong style="font-weight: 600">주소 줄</strong> — "내려받기" + 주소를 글자 그대로 보이는 링크. 단축 주소 · 단추 모양은 쓰지 않습니다(받는 사람이 회사 도메인을 확인할 수 있게)</li>
|
||||
<li><strong style="font-weight: 600">안내 줄</strong> — "내려받을 때 로그인이나 비밀번호를 묻지 않습니다." 12px 회색</li>
|
||||
<li><strong style="font-weight: 600">영어 한 줄</strong> — 거래처가 외국 회사일 수 있어서</li>
|
||||
</ol>
|
||||
<h3 style="margin: 4px 0 0; font-size: 13px; font-weight: 700">모양</h3>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>테두리 1px #D0D5DD 상자 + 글자. 안 여백 12 · 16. 뱃지 · 알약 · 그림 · 아이콘 · 단추 없음(그림은 받는 쪽에서 외부 이미지로 막힙니다)</li>
|
||||
<li>보내는 서식의 허용 목록 안에서만 — table · td(colspan) · a, style 은 테두리 · 여백 · 색 · 글자 크기 · 굵기 · 정렬 · 줄바꿈 막기. 둥근 모서리가 허용 목록에 없어 모서리는 곧습니다</li>
|
||||
<li>글꼴은 본문과 같은 맑은 고딕, 글자 14 · 13 · 12. 상자 폭은 내용에 맞춥니다 — 주소 줄(약 75자)이 가장 깁니다. 좁은 화면에서는 받는 프로그램이 줄을 끊습니다</li>
|
||||
</ul>
|
||||
<h3 style="margin: 4px 0 0; font-size: 13px; font-weight: 700">서버가 넣는다</h3>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>정화 · 서명 그림 · 중복 확인을 마친 뒤에 넣습니다 — 쓰기 화면의 편집기에는 보이지 않고, 쓰는 사람은 첨부 칸의 링크 파일로 봅니다</li>
|
||||
<li>자리는 쓰기 화면이 알려 준 내 글과 인용의 경계 — 내 글 · 서명 아래, 인용 위. 인용이 없으면 맨 끝</li>
|
||||
<li>보이지 않는 표시 <code style="font-family: Consolas, 'Courier New', monospace; font-size: 12px"><!--ara-large-files:v1--></code> … <code style="font-family: Consolas, 'Courier New', monospace; font-size: 12px"><!--/ara-large-files--></code> — 보내는 정화가 주석을 지우므로 사용자가 흉내 낼 수 없습니다</li>
|
||||
<li>보내기 취소로 되돌린 메일은 덩이를 떼고 쓰기 화면의 링크 파일로 돌려줍니다(다시 보내면 새 링크)</li>
|
||||
<li>나중에 링크를 끊어도 본문의 날짜 · 주소는 그대로입니다 — 누르면 내려받기 화면이 "공유 멈춤"을 보입니다</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; align-items: start">
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">텍스트 대체 본문 — 같은 내용</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">글자로만 읽는 메일 프로그램 · 미리 보기 · 인쇄에 쓰인다</p>
|
||||
</div>
|
||||
<pre style="margin: 0; padding: 16px 18px; border: 1px solid #EEF0F3; border-radius: 8px; background: #FBFBFC; font-family: Consolas, 'D2Coding', 'Courier New', 'Malgun Gothic', monospace; font-size: 12.5px; line-height: 1.65; color: #1F2733; white-space: pre-wrap; word-break: break-all">김민수 과장님, 안녕하세요.
|
||||
|
||||
요청하신 설계도와 현장 영상 보내드립니다.
|
||||
파일이 커서 아래 주소로 내려받으실 수 있습니다.
|
||||
|
||||
김아라 · 인프라팀 · ARA SYSTEM
|
||||
010-1234-5678
|
||||
|
||||
----------------------------------------
|
||||
대용량 첨부 2개 (합계 1.8 GB)
|
||||
2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간)
|
||||
- 설계도_v3.dwg (1.2 GB)
|
||||
- 현장영상_1029.mp4 (612 MB)
|
||||
내려받기: https://files.arasystem.it.kr/s/Qx7mK2vR9pL4wT8nB3cF6hJ1dS5gA0eY2uI7oP9kZ3x
|
||||
내려받을 때 로그인이나 비밀번호를 묻지 않습니다.
|
||||
Large files: download by Nov 29, 2026 (KST) — no login or password needed.
|
||||
----------------------------------------
|
||||
|
||||
> 원본 메일
|
||||
> 보낸 사람 | 김민수 <minsu.kim@hanbit-partners.co.kr>
|
||||
> 날짜 | 2026. 10. 29. (목) 오후 5:02
|
||||
> 제목 | 11월 현장 점검 자료 요청
|
||||
> 김아라 매니저님, 11월 현장 점검 자료(설계도 · 현장 영상)를 보내 주실 수 있을까요?</pre>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>자리 · 내용은 HTML 덩이와 같습니다. 위아래를 대시 줄로 가르고, 파일은 "- 이름 (크기)", 주소는 글자 그대로(텍스트 프로그램이 알아서 링크로 만듭니다)</li>
|
||||
<li>되돌린 초안에서 뗄 때는 주소의 토큰으로 그 링크의 덩이인지 확인한 뒤 뗍니다 — 사용자가 같은 글을 쳐 넣었어도 지우지 않게</li>
|
||||
<li>덩이를 넣은 뒤 본문 한도(서식 2,000,000바이트 · 글자 100,000자)를 다시 봅니다 — 쓰기 화면은 덩이 몫(파일 10개면 수 KB)을 남겨 두고 맞춥니다</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 20px">
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">답장 · 전달할 때 — 인용 속에 남은 덩이</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">우리 웹메일의 답장 쓰기 화면 · 박서준이 김아라의 메일에 답장</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; border: 1px solid #EEF0F3; border-radius: 10px; overflow: hidden">
|
||||
<div style="padding: 12px 20px 16px; font-family: 'Malgun Gothic', '맑은 고딕', 'Apple SD Gothic Neo', sans-serif; font-size: 14px; line-height: 1.75; color: #1F2733">
|
||||
<p style="margin: 0">아라님, 받았습니다. 금요일까지 검토하겠습니다.</p>
|
||||
<p style="margin: 0"><br></p>
|
||||
<p style="margin: 0">박서준 · 품질팀</p>
|
||||
<blockquote style="margin: 16px 0 0; padding: 2px 0 2px 14px; border-left: 2px solid #D0D5DD; color: #4A5363">
|
||||
<div style="margin: 0 0 10px; font-size: 12.5px; line-height: 1.6">
|
||||
<p style="margin: 0; font-weight: 600">원본 메일</p>
|
||||
<table style="border-collapse: collapse; font-size: 12.5px; line-height: 1.6">
|
||||
<tbody>
|
||||
<tr><th scope="row" style="padding: 0 12px 0 0; text-align: left; font-weight: 400; color: #667085; white-space: nowrap">보낸 사람</th><td style="padding: 0">김아라 <ara.kim@arasystem.it.kr></td></tr>
|
||||
<tr><th scope="row" style="padding: 0 12px 0 0; text-align: left; font-weight: 400; color: #667085; white-space: nowrap">제목</th><td style="padding: 0">11월 현장 점검 자료 보내드립니다</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p style="margin: 0">서준님, 현장 점검 자료입니다. 설계도와 영상은 링크로 보냅니다.</p>
|
||||
<table style="margin: 10px 0 0; border-collapse: collapse; border: 1px solid #D0D5DD; font-size: 12.5px">
|
||||
<tbody>
|
||||
<tr><td colspan="2" style="padding: 8px 12px 0; font-weight: 700; color: #171C26">대용량 첨부 2개 (합계 1.8 GB)</td></tr>
|
||||
<tr><td colspan="2" style="padding: 0 12px 4px; color: #344054">2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간)</td></tr>
|
||||
<tr><td style="padding: 0 0 0 12px; color: #171C26">설계도_v3.dwg</td><td style="padding: 0 12px 0 12px; color: #667085; text-align: right">1.2 GB</td></tr>
|
||||
<tr><td style="padding: 0 0 0 12px; color: #171C26">현장영상_1029.mp4</td><td style="padding: 0 12px 0 12px; color: #667085; text-align: right">612 MB</td></tr>
|
||||
<tr><td colspan="2" style="padding: 4px 12px 8px; color: #171C26">내려받기 <a href="SharePage.dc.html" style="color: #0E7C86">https://files.arasystem.it.kr/s/Qx7mK2…</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</blockquote>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; padding: 6px 20px 10px; border-top: 1px solid #EEF0F3">
|
||||
<span style="display: inline-flex; align-items: center; gap: 6px; min-height: 40px; color: #344054; font-weight: 500"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>파일 첨부</span>
|
||||
<span style="font-size: 12px; color: #667085">첨부 없음 — 답장은 링크 파일을 다시 싣지 않습니다</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>답장은 링크 파일을 다시 싣지 않습니다(일반 첨부와 같은 관례). 인용 속 덩이는 표째 글로 남고, 그 링크는 처음 기간까지 열립니다</li>
|
||||
<li>인용으로 옮길 때 보이지 않는 표시는 지워집니다 — 이 답장을 받는 쪽에서는 이 덩이가 그냥 인용된 글입니다(첨부 칸으로 올라오지 않음)</li>
|
||||
<li>전달도 같습니다 — 원래 링크는 인용 속 글로 함께 가고 첨부 칸에 다시 싣지 않습니다. 쓰기 화면은 칸 아래 한 줄로만 알립니다("원래 메일의 대용량 첨부 2개는 인용 속 링크로 함께 갑니다 — 11월 29일까지")</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">바깥 사람의 답장을 우리 웹메일에서 읽을 때</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">김민수의 메일 프로그램이 김아라의 메일을 통째로 인용했다</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 6px; padding: 14px 18px; border: 1px solid #EEF0F3; border-radius: 10px; font-family: 'Malgun Gothic', '맑은 고딕', 'Apple SD Gothic Neo', sans-serif; font-size: 14px; line-height: 1.7; color: #1F2733">
|
||||
<p style="margin: 0">잘 받았습니다. 감사합니다.</p>
|
||||
<p style="margin: 6px 0 0; font-size: 12.5px; color: #667085">2026년 10월 30일 (금) 오후 2:15, 김아라 <ara.kim@arasystem.it.kr>님이 작성:</p>
|
||||
<div style="padding: 2px 0 2px 12px; border-left: 2px solid #C9CDD3; color: #4A5363; font-size: 13px">
|
||||
<p style="margin: 0">김민수 과장님, 안녕하세요. 요청하신 설계도와 …</p>
|
||||
<p style="margin: 6px 0 0; padding: 6px 10px; border: 1px solid #D0D5DD">대용량 첨부 2개 (합계 1.8 GB) · 2026년 11월 29일(일)까지 … 내려받기 <span style="color: #0E7C86">https://files.arasystem.it.kr/s/Qx7mK2…</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">인용 속 덩이는 본문 그대로 둡니다 — 첨부 칸으로 올리지 않고 접지도 않습니다. 첨부처럼 올리는 것은 링크 주인이 그 메일의 보낸 사람일 때뿐입니다(이 메일은 김민수가 보냈고 링크 주인은 김아라).</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":1440,"height":2020}}'>
|
||||
class Component extends DCLogic {
|
||||
renderVals() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,296 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>읽기 화면 · 대용량 첨부</title>
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<style>
|
||||
@font-face{font-family:'Pretendard';src:url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2-variations'),url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2');font-weight:45 920;font-style:normal;font-display:swap}
|
||||
body{margin:0}
|
||||
a{color:#0E7C86}a:hover{color:#0A5F67}
|
||||
:focus-visible{outline:2px solid #0E7C86;outline-offset:2px}
|
||||
.primary{background:#0E7C86;color:#FFFFFF}
|
||||
.primary:hover{background:#0A5F67}
|
||||
.ghost{background:transparent;color:#4A5363}
|
||||
.ghost:hover{background:#F2F4F7;color:#171C26}
|
||||
.danger-solid{background:#B42318;color:#FFFFFF}
|
||||
.danger-solid:hover{background:#912018}
|
||||
.tool{background:transparent;color:#344054}
|
||||
.tool:hover{background:#F2F4F7;color:#171C26}
|
||||
.icon-btn{background:transparent;color:#344054}
|
||||
.icon-btn:hover{background:#F2F4F7;color:#171C26}
|
||||
.line-btn{background:#FFFFFF;color:#344054;border:1px solid #D0D5DD}
|
||||
.line-btn:hover{background:#F2F4F7}
|
||||
.line-danger{background:#FFFFFF;color:#B42318;border:1px solid #D0D5DD}
|
||||
.line-danger:hover{background:#FEF3F2}
|
||||
.fold{background:#F6F7F9;color:#4A5363}
|
||||
.fold:hover{background:#EEF0F3}
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="width: 1440px; height: 1940px; box-sizing: border-box; padding: 48px; display: flex; flex-direction: column; gap: 28px; background: #F6F7F9; color: #171C26; font-family: Pretendard, -apple-system, 'Apple SD Gothic Neo', 'Malgun Gothic', system-ui, sans-serif; font-size: 13px; line-height: 1.5; word-break: keep-all; overflow-wrap: anywhere">
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 6px; max-width: 1060px">
|
||||
<h1 style="margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.02em">읽기 화면 · 대용량 첨부</h1>
|
||||
<p style="margin: 0; font-size: 13.5px; color: #4A5363">링크 주인이 그 메일의 보낸 사람이면 링크 파일을 첨부 칸에 첨부처럼 보이고, 본문의 덩이는 접어 둡니다. 바깥 사람 답장의 인용 속 링크 · 전달된 원래 링크 · 비슷하게 꾸민 덩이(서버에 없는 링크)는 그냥 본문입니다. 보낸 사람 자신에게는 받은 수와 [링크 복사] · [링크 끊기]가 더해집니다. 초안 — 사용자 확인 전.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 860px minmax(0, 1fr); gap: 20px; align-items: start">
|
||||
|
||||
<section aria-label="읽기 화면 — 사내 받는 사람" style="display: flex; flex-direction: column; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px; overflow: hidden">
|
||||
<div style="display: flex; align-items: center; gap: 2px; min-height: 52px; box-sizing: border-box; padding: 4px 24px; border-bottom: 1px solid #EEF0F3">
|
||||
<button type="button" class="tool" style="display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 10px; border: 0; border-radius: 8px; font: inherit; font-weight: 500; cursor: pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 17 4 12 9 7"></polyline><path d="M20 18v-2a4 4 0 0 0-4-4H4"></path></svg><span>답장</span></button>
|
||||
<button type="button" class="tool" style="display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 10px; border: 0; border-radius: 8px; font: inherit; font-weight: 500; cursor: pointer"><span>전체 답장</span></button>
|
||||
<button type="button" class="tool" style="display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 10px; border: 0; border-radius: 8px; font: inherit; font-weight: 500; cursor: pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 17 20 12 15 7"></polyline><path d="M4 18v-2a4 4 0 0 1 4-4h12"></path></svg><span>전달</span></button>
|
||||
<span aria-hidden="true" style="width: 1px; height: 20px; margin: 0 8px; background: #E6E8EC"></span>
|
||||
<button type="button" class="tool" style="display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 10px; border: 0; border-radius: 8px; font: inherit; font-weight: 500; cursor: pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg><span>스팸 신고</span></button>
|
||||
<button type="button" class="tool" style="display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 10px; border: 0; border-radius: 8px; color: #B42318; font: inherit; font-weight: 500; cursor: pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg><span>삭제</span></button>
|
||||
<button type="button" class="tool" aria-label="더 보기" style="display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; border: 0; border-radius: 8px; cursor: pointer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg></button>
|
||||
</div>
|
||||
|
||||
<article aria-labelledby="rl-subject" style="display: flex; flex-direction: column; gap: 18px; max-width: 800px; box-sizing: border-box; padding: 20px 32px 32px">
|
||||
<h2 id="rl-subject" style="margin: 4px 0 0; font-size: 20px; line-height: 1.4; font-weight: 650; letter-spacing: -0.015em">11월 현장 점검 자료 보내드립니다</h2>
|
||||
<div style="display: flex; align-items: flex-start; gap: 12px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 50%; background: #E3F2F3; color: #0A5F67; font-size: 14px; font-weight: 600">김</span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px">
|
||||
<span style="font-size: 13.5px; font-weight: 600">김아라</span>
|
||||
<span style="font-size: 12.5px; color: #667085">ara.kim@arasystem.it.kr</span>
|
||||
<span style="margin-left: auto; font-size: 12.5px; color: #667085; white-space: nowrap; font-variant-numeric: tabular-nums">2026. 10. 30. (금) 오후 2:15</span>
|
||||
</div>
|
||||
<span style="font-size: 12.5px; color: #4A5363">받는 사람 박서준 <seojun.park@arasystem.it.kr></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; max-width: 680px; font-family: 'Malgun Gothic', '맑은 고딕', 'Apple SD Gothic Neo', sans-serif; font-size: 14px; line-height: 1.75; color: #1F2733">
|
||||
<p style="margin: 0">서준님, 현장 점검 자료입니다.</p>
|
||||
<p style="margin: 0">견적서는 첨부했고, 설계도와 영상은 용량이 커서 링크로 보냅니다.</p>
|
||||
<p style="margin: 0"><br></p>
|
||||
<p style="margin: 0">김아라 · 인프라팀 · ARA SYSTEM</p>
|
||||
<button type="button" class="fold" aria-expanded="false" style="display: flex; align-items: center; gap: 8px; margin-top: 14px; min-height: 40px; padding: 0 12px 0 10px; border: 0; border-radius: 6px; font-family: Pretendard, -apple-system, 'Malgun Gothic', sans-serif; font-size: 12.5px; text-align: left; cursor: pointer">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
||||
<span style="flex: 1 1 auto">본문의 대용량 첨부 안내 (접어 둠) — 파일은 아래 첨부 칸에 있습니다</span>
|
||||
<span style="flex: 0 0 auto; color: #0A5F67; font-weight: 600">펼치기</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section aria-labelledby="rl-att-title" style="display: flex; flex-direction: column; gap: 8px; padding-top: 16px; border-top: 1px solid #EEF0F3">
|
||||
<h3 id="rl-att-title" style="margin: 0; font-size: 12.5px; font-weight: 600; color: #4A5363">첨부 파일 3개</h3>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr)); gap: 8px">
|
||||
<li style="display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 8px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">견적서_11월.xlsx</span><span style="font-size: 12px; color: #667085">52 KB</span></div>
|
||||
<button type="button" class="icon-btn" aria-label="견적서_11월.xlsx 다운로드" style="flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 8px; cursor: pointer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 8px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">설계도_v3.dwg</span><span style="font-size: 12px; color: #667085">1.2 GB · 11월 29일까지</span></div>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 다운로드 — 11월 29일까지" style="flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 8px; cursor: pointer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 8px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">현장영상_1029.mp4</span><span style="font-size: 12px; color: #667085">612 MB · 11월 29일까지</span></div>
|
||||
<button type="button" class="icon-btn" aria-label="현장영상_1029.mp4 다운로드 — 11월 29일까지" style="flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 8px; cursor: pointer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 20px">
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">사내 받는 사람에게</h2>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>링크 파일은 첨부 칸에 일반 첨부와 함께 — 이름 아래 "크기 · 11월 29일까지"(끝나는 날, 한국 시간). 아이콘 · 단추 모양은 일반 첨부와 같습니다</li>
|
||||
<li>[다운로드]는 내려받기 화면을 거치지 않고 바로 받습니다. 받은 수에 들어갑니다(보낸 사람 자신은 빼고)</li>
|
||||
<li>「첨부 파일 N개」는 메일에 담긴 첨부와 링크 파일을 합한 수입니다</li>
|
||||
<li>첨부처럼 올리는 조건 — 주소가 우리 내려받기 주소이고 서버에 있는 링크이며, <strong style="font-weight: 600">링크 주인이 그 메일의 보낸 사람일 때만</strong>. 바깥 사람 답장의 인용 속 링크 · 전달된 원래 링크는 본문 글로 둡니다</li>
|
||||
<li>목록의 클립 아이콘은 지금처럼 메일에 담긴 첨부만 봅니다 — 링크 파일만 있는 메일은 처음 판에 클립이 없습니다</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">덩이를 펼치면</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">본문 틀 안의 접기 줄 — 바깥 사람이 보는 그대로</p>
|
||||
</div>
|
||||
<button type="button" class="fold" aria-expanded="true" style="display: flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 12px 0 10px; border: 0; border-radius: 6px; font: inherit; font-size: 12.5px; text-align: left; cursor: pointer">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
<span style="flex: 1 1 auto">본문의 대용량 첨부 안내</span>
|
||||
<span style="flex: 0 0 auto; color: #0A5F67; font-weight: 600">접기</span>
|
||||
</button>
|
||||
<table style="border-collapse: collapse; border: 1px solid #D0D5DD; font-family: 'Malgun Gothic', '맑은 고딕', sans-serif; font-size: 12.5px; line-height: 1.6">
|
||||
<tbody>
|
||||
<tr><td colspan="2" style="padding: 8px 12px 0; font-weight: 700; color: #171C26">대용량 첨부 2개 (합계 1.8 GB)</td></tr>
|
||||
<tr><td colspan="2" style="padding: 0 12px 4px; color: #344054">2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간)</td></tr>
|
||||
<tr><td style="padding: 0 0 0 12px">설계도_v3.dwg</td><td style="padding: 0 12px; color: #667085; text-align: right">1.2 GB</td></tr>
|
||||
<tr><td style="padding: 0 0 0 12px">현장영상_1029.mp4</td><td style="padding: 0 12px; color: #667085; text-align: right">612 MB</td></tr>
|
||||
<tr><td colspan="2" style="padding: 4px 12px 8px">내려받기 <a href="SharePage.dc.html">https://files.arasystem.it.kr/s/Qx7mK2…</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">접기 줄은 본문 틀(격리된 iframe) 안에서 스크립트 없이 열고 닫힙니다. 높이 40, 글 12.5px. 메일을 다시 열면 다시 접혀 있습니다. 인쇄 화면은 펼친 채로 인쇄합니다(종이에는 주소가 있어야 하므로).</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 860px minmax(0, 1fr); gap: 20px; align-items: start">
|
||||
|
||||
<section aria-label="읽기 화면 — 보낸 사람 자신(보낸편지함)" style="display: flex; flex-direction: column; gap: 12px; padding: 20px 32px 24px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">보낸 사람 자신 — 보낸편지함에서 같은 메일</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">첨부 칸만 보입니다 · 위쪽(머리 · 본문 · 접기 줄)은 받는 사람과 같습니다</p>
|
||||
</div>
|
||||
<section aria-labelledby="rl-sent-att" style="display: flex; flex-direction: column; gap: 8px; padding-top: 16px; border-top: 1px solid #EEF0F3">
|
||||
<h3 id="rl-sent-att" style="margin: 0; font-size: 12.5px; font-weight: 600; color: #4A5363">첨부 파일 3개</h3>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr)); gap: 8px">
|
||||
<li style="display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 8px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">견적서_11월.xlsx</span><span style="font-size: 12px; color: #667085">52 KB</span></div>
|
||||
<button type="button" class="icon-btn" aria-label="견적서_11월.xlsx 다운로드" style="flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 8px; cursor: pointer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 8px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">설계도_v3.dwg</span><span style="font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">1.2 GB · 3회 받음 · 11월 29일까지</span></div>
|
||||
<button type="button" class="icon-btn" aria-label="설계도_v3.dwg 다운로드" style="flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 8px; cursor: pointer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 8px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">현장영상_1029.mp4</span><span style="font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">612 MB · 아직 받지 않음 · 11월 29일까지</span></div>
|
||||
<button type="button" class="icon-btn" aria-label="현장영상_1029.mp4 다운로드" style="flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 8px; cursor: pointer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
||||
</li>
|
||||
</ul>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 4px 0 0">
|
||||
<span id="rl-link-note" style="flex: 1 1 320px; font-size: 12.5px; color: #4A5363">링크로 보낸 파일 2개 — 받는 사람 모두 같은 링크로 받습니다. 끊으면 둘 다 받을 수 없습니다.</span>
|
||||
<span style="flex: 0 0 auto; display: inline-flex; gap: 6px">
|
||||
<button type="button" class="line-btn" style="min-height: 36px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">링크 복사</button>
|
||||
<button type="button" class="line-danger" aria-describedby="rl-link-note" style="min-height: 36px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">링크 끊기</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>받은 수는 실제로 받기 시작한 수입니다 — 화면 열기 · 이어 받기 · 내가 받은 것은 세지 않습니다. 누가 받았는지는 남기지 않습니다</li>
|
||||
<li>[링크 복사] · [링크 끊기]는 메일에 한 번(36 — 차단한 주소의 [차단 풀기]와 같은 밀도 예외, 새 토큰 없음). 링크가 메일 한 통에 하나라 파일마다 두지 않습니다</li>
|
||||
<li>[링크 복사]는 이 메일 본문의 덩이에서 주소를 읽어 복사합니다 — 알림 "링크를 복사했습니다."(못 하면 "링크를 복사하지 못했습니다."). 설정 › 대용량 첨부에는 없습니다(서버는 링크를 해시로만 압니다)</li>
|
||||
<li>사내 받는 사람에게는 이 줄과 받은 수가 없습니다. 보낸편지함이 아니어도(내가 나에게 보낸 메일 등) 링크 주인이면 보입니다</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 14px">
|
||||
<div style="display: flex; justify-content: center; padding: 24px 16px; border-radius: 12px; background: rgba(16, 24, 28, 0.08)">
|
||||
<div role="alertdialog" aria-modal="true" aria-labelledby="rl-cut-title" aria-describedby="rl-cut-message" style="width: 400px; box-sizing: border-box; padding: 18px 20px 14px; border: 1px solid #E6E8EC; border-radius: 12px; background: #FFFFFF; box-shadow: 0 14px 36px rgba(16, 24, 28, 0.14); display: flex; flex-direction: column; gap: 10px">
|
||||
<h3 id="rl-cut-title" style="margin: 0; font-size: 15px; font-weight: 700">대용량 첨부 링크를 끊을까요?</h3>
|
||||
<p id="rl-cut-message" style="margin: 0; color: #4A5363">받는 사람 모두 이 메일의 파일 2개(설계도_v3.dwg · 현장영상_1029.mp4)를 더는 내려받을 수 없습니다. 이미 받은 파일은 되돌릴 수 없고, 끊은 링크는 다시 열 수 없습니다.</p>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px">
|
||||
<button type="button" class="danger-solid" style="min-height: 44px; padding: 0 16px; border: 0; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer">링크 끊기</button>
|
||||
<button type="button" class="ghost" style="min-height: 44px; padding: 0 14px; border: 0; border-radius: 8px; font: inherit; font-weight: 500; cursor: pointer; outline: 2px solid #0E7C86; outline-offset: 2px">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">되돌릴 수 없는 일이라 처음 초점은 「취소」(파란 고리) · Esc 도 취소입니다. 끊으면 알림 하나 — 되돌리기 단추는 없습니다:</p>
|
||||
<div role="status" style="display: flex; align-items: center; gap: 10px; min-height: 44px; box-sizing: border-box; padding: 6px 14px; border-radius: 10px; background: #0B3640; color: #FFFFFF; box-shadow: 0 10px 28px rgba(16, 24, 28, 0.22)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9FE0B5" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; font-weight: 500">링크를 끊었습니다. 받는 사람은 더 이상 내려받을 수 없습니다.</span>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">끊은 뒤 첨부 칸은 아래 "공유 멈춤" 모양이 되고, 쓰던 보관 공간은 바로 비워집니다. 같은 확인 창을 설정 › 메일 › 대용량 첨부에서도 씁니다(창 글에 메일 제목).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; align-items: start">
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">기간 지남 · 공유 멈춤</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">흐린 글자 + 까닭 · 내려받기 단추 없음</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px">
|
||||
<li style="display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 12px 4px 8px; border: 1px solid #EEF0F3; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F6F7F9; color: #98A2B3"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #667085">설계도_v3.dwg</span><span style="font-size: 12px; color: #667085">1.2 GB · 기간 지남 — 11월 29일까지였습니다</span></div>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 12px 4px 8px; border: 1px solid #EEF0F3; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F6F7F9; color: #98A2B3"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #667085">현장영상_1029.mp4</span><span style="font-size: 12px; color: #667085">612 MB · 공유 멈춤 — 보낸 사람이 링크를 끊었습니다</span></div>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 12px 4px 8px; border: 1px solid #EEF0F3; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F6F7F9; color: #98A2B3"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #667085">설계도_v3.dwg</span><span style="font-size: 12px; color: #667085">1.2 GB · 3회 받음 · 공유 멈춤 — 10월 31일에 끊었습니다</span></div>
|
||||
</li>
|
||||
</ul>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">셋째 줄은 보낸 사람 자신이 본 모양입니다(받은 수는 남습니다). 흐리게는 투명도가 아니라 글자색 #667085 · 아이콘 #98A2B3(대비 4.5:1 이상). 본문의 접기 줄은 그대로 둡니다. 끝난 뒤 90일이 지나면 서버가 그 링크를 잊어 첨부 칸에서 빠지고 덩이는 그냥 본문이 됩니다.</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">보안 검사 — 받는 사람에게</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">파일마다 · 글자로만</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px">
|
||||
<li style="display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 8px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">현장영상_1029.mp4</span><span id="rl-scan-note" style="font-size: 12px; color: #667085">612 MB · 보안 검사 중 — 끝나면 받을 수 있습니다</span></div>
|
||||
<button type="button" aria-disabled="true" aria-label="현장영상_1029.mp4 다운로드" aria-describedby="rl-scan-note" title="보안 검사가 끝나면 받을 수 있습니다" style="flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 8px; background: transparent; color: #98A2B3; cursor: not-allowed"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 12px 4px 8px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #FFFFFF; color: #B42318"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #B42318">견적_요청서.zip</span><span style="font-size: 12px; color: #B42318">위험 — 보안 검사에서 막았습니다. 받을 수 없습니다</span></div>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 8px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">협력사_자료모음.7z</span><span style="font-size: 12px; color: #667085">860 MB · 11월 26일까지 · 검사하지 못함 — 암호가 걸린 압축 파일</span></div>
|
||||
<button type="button" class="icon-btn" aria-label="협력사_자료모음.7z 다운로드" style="flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 8px; cursor: pointer"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
||||
</li>
|
||||
</ul>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">검사 중이면 [다운로드]를 끄고(aria-disabled — 초점은 받고, 누르면 까닭을 읽어 줍니다) 끝나는 날 대신 까닭을 씁니다. 메일을 다시 열거나 새 메일 확인 때 상태를 다시 받습니다. 검사하지 못한 파일은 받을 수 있습니다 — 글로만 알립니다.</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">전달 쓰기 화면 — 원래 링크는 인용 속 글로</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">박서준이 이 메일을 바깥 협력사에 전달 · 첨부 칸에 다시 싣지 않는다</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; padding: 10px 14px 12px; border: 1px solid #EEF0F3; border-radius: 10px">
|
||||
<div style="display: flex; align-items: center; gap: 6px; min-height: 36px; color: #344054; font-weight: 500"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg><span>파일 첨부</span></div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 2px 2px 2px 10px; border: 1px solid #E6E8EC; border-radius: 8px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">견적서_11월.xlsx</span>
|
||||
<span style="flex: 0 0 auto; font-size: 12px; color: #667085">52 KB</span>
|
||||
<button type="button" class="icon-btn" aria-label="견적서_11월.xlsx 첨부 취소" style="flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 6px; cursor: pointer"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
||||
</div>
|
||||
<p role="status" style="display: flex; align-items: flex-start; gap: 6px; margin: 0; font-size: 12.5px; color: #4A5363">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto; margin-top: 2px"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
<span>원래 메일의 대용량 첨부 2개는 인용 속 링크로 함께 갑니다 — 11월 29일까지</span>
|
||||
</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 6px; padding: 10px 14px 12px; border: 1px dashed #D0D5DD; border-radius: 10px">
|
||||
<span style="font-size: 12px; color: #667085">공유가 멈췄거나 기간이 지난 링크를 인용한 전달이면</span>
|
||||
<p role="status" style="display: flex; align-items: flex-start; gap: 6px; margin: 0; font-size: 12.5px; color: #7A5200">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto; margin-top: 2px"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
||||
<span>인용 속 ‘도면_초안.dwg’ 링크는 공유가 멈춰 받을 수 없습니다.</span>
|
||||
</p>
|
||||
<p role="status" style="display: flex; align-items: flex-start; gap: 6px; margin: 0; font-size: 12.5px; color: #7A5200">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto; margin-top: 2px"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
||||
<span>인용 속 ‘행사사진_원본.zip’ 링크는 기간이 지나 받을 수 없습니다.</span>
|
||||
</p>
|
||||
</div>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>원래 메일의 덩이는 인용 속 표 · 링크로 그대로 갑니다 — 첨부 칸에 다시 싣지 않고 칸 아래 한 줄로만 알립니다(링크 파일 수 · 끝나는 날). 파일이 여럿인 끝난 링크는 "‘설계도_v3.dwg’ 외 1개"</li>
|
||||
<li>끝난 링크도 보내기는 막지 않습니다 — 갈색 한 줄로만 알립니다</li>
|
||||
<li>전달하는 사람이 새 큰 파일을 더하면 그것은 이 메일의 새 링크입니다 — 첨부 칸의 「링크로 보내는 파일」(전달하는 사람의 보관 한도에 잡힘)</li>
|
||||
<li>받는 쪽에서 인용 속 원래 링크는 본문 글입니다(링크 주인이 보낸 메일이 아니라서 첨부 칸으로 올리지 않음). 내려받기 화면의 보낸 사람 · 끊을 수 있는 사람은 처음 보낸 사람(링크 주인)입니다. 답장도 같습니다</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":1440,"height":1940}}'>
|
||||
class Component extends DCLogic {
|
||||
renderVals() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -495,7 +495,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
|
||||
### 보내기 취소 · 예약 발송 (`MailSendLater`)
|
||||
|
||||
- **[보내기 ▾]**: 쓰기 화면의 [보내기]를 둘로 나눈다 — 왼쪽은 지금의 [보내기], 오른쪽 ▾(44 — 보드는 40, 밀도 예외를 새로 만들지 않는다)는 메뉴 「예약 발송…」(시계 아이콘). Ctrl+Enter 는 그대로 [보내기].
|
||||
- **[보내기 ▾]**: 쓰기 화면의 [보내기]를 둘로 나눈다 — 왼쪽은 지금의 [보내기], 오른쪽 ▾(44 — 보드는 40, 밀도 예외를 새로 만들지 않는다)는 메뉴 「예약 발송…」(시계 아이콘). 보내기 단축키는 없다(쓰기 화면에 Ctrl+Enter 묶음이 없다 — 2026-10-30 확인).
|
||||
보내는 중 · 저장 중 · 첨부를 올리는 중에는 두 단추를 함께 끈다(지금의 [보내기]와 같은 규칙).
|
||||
- **예약 창**(모달 400): 제목 "예약 발송". 고르기 넷 — 「내일 오전」 9:00 · 「내일 오후」 1:00 · 「다음 주 월요일」 9:00 · 「직접 고르기」(날짜 칸 + 30분 단위 시각) — 줄마다 오른쪽에 날짜 · 요일 · 시각.
|
||||
오늘 오후가 남았으면 「오늘 오후」 6:00 이 첫 줄. 안내 "이 PC 의 시간(한국 표준시)으로 보냅니다 · 7일 안에서 고를 수 있습니다." [예약] [취소].
|
||||
@@ -523,6 +523,145 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- 목록: 줄마다 주소(굵게) + 아래 작은 글 "10월 29일 차단"(도메인은 "도메인 전체 · …") + [차단 풀기](36). 누르면 바로 풀고 알림 "‘주소’ 차단을 풀었습니다." + [되돌리기].
|
||||
이미 받은 메일은 옮기지 않는다. 비었으면 "차단한 주소가 없습니다. 메일의 ⋯ › 보낸 사람 차단으로도 막을 수 있습니다."
|
||||
|
||||
## 11단계 — 대용량 첨부 (2026-10-30 확정 — `ComposeLargeFiles` · `LargeFileBlock` · `MailReadLargeFiles` · `SharePage` · `SettingsLargeFiles`)
|
||||
|
||||
메일에 담지 못하는 큰 파일을 링크로 보낸다. 파일은 회사 서버에 조각으로 올리고(끊겨도 이어서), 보낼 때 서버가 본문에 내려받기 안내(덩이)를 넣는다.
|
||||
받는 사람은 로그인 없이 내려받기 화면 `files.arasystem.it.kr/s/{토큰}` 에서 받는다. 보관은 보낸 때(예약 발송은 보낼 시각)부터 30일째 날 23:59:59(한국 시간)까지 — 받는 사람 모두 같은 날이다.
|
||||
한도는 파일당 2GB · 메일 한 통에 링크 파일 10개 · 직원마다 동시 보관 10GB(올리는 중 · 임시 저장 포함) — 모두 운영 설정이다.
|
||||
제품 · 기술 결정은 2026-10-30 확정본과 같은 날 정한 보완([링크 복사]는 읽기 화면에서만 · 보내지 않은 파일 지우기 · 전달은 인용 속 링크로 · 첨부처럼 올리는 규칙 — 아래 각 절)을 따른다.
|
||||
데이터 · 규칙은 계약 4절(`docs/mail/api-contract.md` "대용량 첨부 — 11단계")이 정하고, 아래 문구가 새 에러 코드(`FILE_003`~`FILE_008` · `MAIL_016` — 계약 4절 "에러 코드")의 사전 문구 초안이다.
|
||||
새 아이콘: 멈춤(pause) · 이어 올리기(play) · 경고 삼각형(alert-triangle). 새 토큰은 없다 — 밀도 예외도 있는 값(40 · 36)을 다시 쓴다.
|
||||
|
||||
### 쓰기 화면 — 첨부 칸 (`ComposeLargeFiles`)
|
||||
|
||||
- **링크로 바뀌는 때**: 파일 하나가 20MB 를 넘을 때, 메일에 담은 파일의 합계를 25MB 넘게 만들 때(고른 차례대로 그 파일부터), 메일에 담지 못하는 형식(mp4 · mov · dwg · dxf · psd · ai · 7z)일 때 저절로 링크로 보낸다.
|
||||
묻는 창은 없다 — 합계 때문에 바뀌면 칸 아래 ⓘ 한 줄 "‘회의자료_전체.pptx’(9.4 MB)는 메일에 담으면 합계가 25MB 를 넘어 링크로 보냅니다."(role=status). 받는 사람이 사내뿐이어도 같은 규칙이다.
|
||||
메일에 담은 파일도 줄의 링크 단추(40 · 「링크로 보내기」 — 마우스를 올리면 같은 글)로 바꿀 수 있다 — **이 쓰기 화면에서 고른 파일에만** 있다(전달 원본 · 저장본에서 이어 온 첨부에는 없다 — 처음 판).
|
||||
링크 → 메일에 담기 단추는 없다(빼고 다시 고르면 한도 안에서 담긴다 — 아래 "처음 판에서 미룬 것").
|
||||
- **안내문**: "파일당 최대 20MB · 합계 25MB · 최대 20개 · 더 큰 파일은 링크로 보냅니다 (파일당 2GB · 10개까지)".
|
||||
- **두 묶음**: 링크 파일이 있으면 칸을 「메일에 담는 파일 N개」(+ "합계 18.1 MB / 25 MB") · 「링크로 보내는 파일 N개」(+ "받는 사람은 보낸 날부터 30일 동안 내려받습니다 — 오늘 보내면 11월 29일(일)까지")로 나눈다(12.5 굵게 + 12 회색).
|
||||
일반 첨부만 있으면 지금처럼 묶음 이름 없이 — 링크 파일만 있어도 「링크로 보내는 파일 N개」 머리는 둔다(기간 안내 · 보관 경고의 자리, 보드대로). 보관 사용량이 80%를 넘으면 링크 묶음 머리 오른쪽에 "보관 8.4 / 10 GB"(갈색 굵게).
|
||||
- **링크 파일 줄**: 아이콘이 링크(청록 #0A5F67), 두 줄 — 이름 · 단추(40) / 크기 · 상태(12px, 아이콘 선만큼 26px 들여), 올리는 동안 3px 막대. ✕ 는 「빼기」 — 올리던 것을 멈추고 서버의 조각도 지운다.
|
||||
|
||||
| 상태 | 둘째 줄 | 단추 · 모양 |
|
||||
| --- | --- | --- |
|
||||
| 올리는 중 | "올리는 중 · 420 MB / 1.2 GB" + 오른쪽 "약 3분 남음"(처음 몇 초는 "시간 계산 중") | [멈춤] ✕ · 청록 막대 |
|
||||
| 멈춤 | "멈춤 · 420 MB / 1.2 GB 올림" | [이어 올리기] ✕ · 회색 막대 — 그 파일만 멈춘다 |
|
||||
| 잠깐 끊김 | "연결이 끊겨 다시 잇는 중… · 420 MB / 1.2 GB 올림"(갈색) | 누를 것 없음 — 2 · 5 · 15 · 30초 간격으로 저절로, 받은 조각은 그대로 |
|
||||
| 오래 끊김 | "올리지 못하고 멈췄습니다 — 올린 420 MB 는 남아 있습니다."(빨강) | 빨간 줄 · [다시 시도](40) ✕ — 저절로 다시 잇기를 1분 넘게 못 했을 때 |
|
||||
| 다 올림 · 검사 중 | "612 MB · 보안 검사 중 — 끝나지 않아도 보낼 수 있습니다" | ✕ |
|
||||
| 검사 끝 | 깨끗하면 크기만 "1.2 GB" · 못 했으면 "860 MB · 검사하지 못함 — 암호가 걸린 압축 파일 등"(까닭은 응답에 없다 — 암호 압축 · 한도 밖 · 검사기 없음을 함께 쓴다) | ✕ |
|
||||
| 위험 | "위험한 내용이 발견되어 보낼 수 없습니다. 이 파일을 빼 주세요."(경고 삼각형) | 빨간 줄 · ✕ |
|
||||
|
||||
- **올리기 전 검사**(지금 첨부 사전 검사와 같은 결 — 넘으면 올리지 않고 빨간 줄, 그 파일만 빠지고 보내기는 막지 않는다):
|
||||
"파일 하나는 2GB 까지 보낼 수 있습니다." · "링크로 보내는 파일은 메일 한 통에 10개까지입니다." ·
|
||||
"대용량 보관 한도(10GB)를 넘어 올릴 수 없습니다. 보내지 않은 파일을 지우거나 링크를 끊으면 바로 공간이 생깁니다. [대용량 첨부 보기]" ·
|
||||
"지금은 서버에 공간이 모자라 올릴 수 없습니다. 잠시 후 다시 시도해 주세요." · 형식은 `FILE_001` 사전 문구.
|
||||
링크로 보낼 수 있는 형식 = 일반 첨부 목록 + mp4 · mov · dwg · dxf · psd · ai · 7z. 실행 파일(exe · msi · bat · cmd · js · vbs · scr 등)과 디스크 이미지(iso · img · vhd)는 막는다. 파일 고르기 창은 두 목록을 합친 형식만.
|
||||
- **다시 열 때**: 브라우저는 고른 파일을 스스로 다시 읽지 못한다. 새로고침 · 창을 닫은 뒤 저장본을 열면 노란 줄 "올리다 멈춘 파일 — 같은 파일을 다시 고르면 이어서 올립니다 · 420 MB / 1.2 GB 올림" + [파일 고르기](40) ✕.
|
||||
다른 파일을 고르면 그 줄 안에 "고른 파일이 멈춘 파일과 다릅니다(이름 · 크기). 같은 파일을 골라 주세요."(이름 · 크기 · 고친 시각으로 맞춘다 — 서버는 마지막 조각 뒤 24시간 기다린다).
|
||||
다 올린 파일은 저장본에 붙어 "1.2 GB · 이 임시 저장본에 11월 29일까지 남습니다"(올린 날부터 30일), 지나면 빨간 줄 "기간이 지나 지운 파일입니다 — 빼고 다시 올려 주세요."
|
||||
설정 › 대용량 첨부에서 지웠으면 같은 모양에 글만 "설정 › 대용량 첨부에서 지운 파일입니다 — 빼고 다시 올려 주세요."
|
||||
- **다 올릴 때까지 보내기를 막는다**: [보내기]와 ▾ 를 `disabled` 가 아니라 `aria-disabled` 로 끄고(초점은 받는다) [임시 저장] 오른쪽에 까닭 글(ⓘ 12.5 — 두 단추의 `aria-describedby`).
|
||||
누르면 보내지 않고 까닭을 읽어 준다(role=status — 보내기 단축키는 앱에 없다). 「예약 발송…」도 같다. 임시 저장은 그대로 된다. 까닭 글 —
|
||||
올리는 중 · 다시 잇는 중 "파일 2개를 올리는 중입니다 — 다 올리면 보낼 수 있습니다." ·
|
||||
멈춤 · 오래 끊김 · 다시 열어 멈춘 파일 "다 올리지 못한 파일이 있습니다 — 이어 올리거나 빼 주세요." ·
|
||||
위험 · 지운 파일(기간 지남 · 설정에서 지움) "보낼 수 없는 파일이 있습니다 — 빨간 줄의 파일을 빼 주세요."
|
||||
- **나가기 확인**: 올리는 중이면 "올리는 중인 파일이 있습니다" / "이 화면을 나가면 올리기가 멈춥니다. 임시보관함에서 이 메일을 다시 열고 같은 파일을 고르면 이어서 올립니다." [계속 쓰기](처음 초점 · Esc) [나가기].
|
||||
떠나기 전 임시 저장은 지금처럼 먼저 하고, 올리는 중인 파일은 저장본에 "올리다 멈춘 파일"로 남긴다. 다 올린 뒤에는 이 확인이 없다.
|
||||
|
||||
### 본문 덩이 — 바깥 메일 프로그램에서 (`LargeFileBlock`)
|
||||
|
||||
- **자리**: 내 글 · 서명 아래, 인용 위(쓰기 화면이 알려 준 경계). 인용이 없으면 맨 끝. 서버가 정화 · 서명 그림 · 중복 확인을 마친 뒤 넣는다 — 쓰기 화면의 편집기에는 보이지 않는다.
|
||||
- **내용**(위에서부터): "대용량 첨부 2개 (합계 1.8 GB)"(14 굵게) → "2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간)" → 파일마다 이름 · 크기(회색, 오른쪽) →
|
||||
"내려받기 https://files.arasystem.it.kr/s/…"(주소를 글자 그대로 보이는 링크 — 단축 주소 · 단추 모양 없음. 파일 이름은 링크가 아니다 — 메일 한 통에 링크 하나) →
|
||||
"내려받을 때 로그인이나 비밀번호를 묻지 않습니다."(12 회색) → "Large files: download by Nov 29, 2026 (KST) — no login or password needed."(12 회색).
|
||||
- **모양**: 테두리 1px #D0D5DD 표 + 글자, 안 여백 12 · 16, 맑은 고딕 14 · 13 · 12. 보내는 서식의 허용 목록 안에서만(table · td colspan · a, 테두리 · 여백 · 색 · 크기 · 굵기 · 정렬 · 줄바꿈 막기) —
|
||||
둥근 모서리 · 그림 · 아이콘 · 뱃지 없음. 폭은 내용에 맞춘다(주소 줄이 가장 길다 — 좁은 화면에서는 받는 프로그램이 줄을 끊는다).
|
||||
- **텍스트 대체 본문**: 같은 자리에 대시 줄로 가른 같은 내용 — 파일은 "- 이름 (크기)", 주소는 "내려받기: 주소".
|
||||
- 보이지 않는 표시 `<!--ara-large-files:v1-->` … `<!--/ara-large-files-->` — 보내는 정화가 주석을 지우므로 사용자가 흉내 낼 수 없다. 보내기 취소로 되돌린 메일은 덩이를 떼고 링크 파일로 돌려준다.
|
||||
나중에 링크를 끊어도 본문의 날짜 · 주소는 그대로다(누르면 내려받기 화면이 "공유 멈춤").
|
||||
- **답장 · 전달**: 링크 파일을 첨부 칸에 다시 싣지 않는다 — 인용 속 덩이는 표째 글(링크)로 그대로 가고 링크는 처음 기간까지 열린다. 인용으로 옮기면 표시가 지워진다.
|
||||
전달 쓰기 화면은 칸 아래 한 줄로만 알린다(아래 "읽기 화면 › 전달 쓰기 화면"). 바깥 사람의 답장에 인용된 덩이도 우리 웹메일에서 본문 그대로다 — 첨부 칸으로 올리지 않고 접지도 않는다.
|
||||
|
||||
### 읽기 화면 (`MailReadLargeFiles`)
|
||||
|
||||
- **첨부처럼 올리는 규칙**: 주소가 우리 내려받기 주소이고 서버에 있는 링크이며, **링크 주인이 그 메일의 보낸 사람과 같을 때만** 첨부 칸으로 올린다 —
|
||||
바깥 사람 답장의 인용 속 링크 · 전달된 원래 링크는 본문 글이다.
|
||||
- **사내 받는 사람**: 링크 파일을 첨부 칸에 일반 첨부와 함께 보인다 — 이름 아래 "1.2 GB · 11월 29일까지". 아이콘 · [다운로드](44)는 일반 첨부와 같고, 내려받기 화면을 거치지 않고 바로 받는다. 「첨부 파일 N개」는 둘을 합한 수.
|
||||
- **본문의 덩이는 접는다**: 그 자리에 접기 줄(높이 40 · 12.5 · 바탕 #F6F7F9) "본문의 대용량 첨부 안내 (접어 둠) — 파일은 아래 첨부 칸에 있습니다" + "펼치기".
|
||||
본문 틀 안에서 스크립트 없이 열고 닫는다. 메일을 다시 열면 접혀 있고, 인쇄 화면은 펼친 채 인쇄한다.
|
||||
- **보낸 사람 자신**: 파일마다 "1.2 GB · 3회 받음 · 11월 29일까지"(받지 않았으면 "아직 받지 않음"), 첨부 칸 아래 한 줄
|
||||
"링크로 보낸 파일 2개 — 받는 사람 모두 같은 링크로 받습니다. 끊으면 둘 다 받을 수 없습니다." + [링크 복사] [링크 끊기](36 — 차단한 주소의 [차단 풀기]와 같은 밀도 예외, [링크 끊기]는 빨간 글 테두리 단추).
|
||||
[링크 복사]는 이 메일 본문의 덩이에서 주소를 읽어 복사한다 — "링크를 복사했습니다."(못 하면 "링크를 복사하지 못했습니다."). 설정 목록에는 없다(서버는 링크를 해시로만 안다).
|
||||
받은 수는 처음부터 받기 시작한 수뿐(화면 열기 · 이어 받기 · 내가 받은 것은 빼고)이고, 누가 받았는지는 남기지 않는다.
|
||||
- **링크 끊기 확인**(alertdialog 400): "대용량 첨부 링크를 끊을까요?" / "받는 사람 모두 이 메일의 파일 2개(설계도_v3.dwg · 현장영상_1029.mp4)를 더는 내려받을 수 없습니다. 이미 받은 파일은 되돌릴 수 없고, 끊은 링크는 다시 열 수 없습니다."
|
||||
[링크 끊기](빨강) [취소](처음 초점 · Esc). 끊으면 "링크를 끊었습니다. 받는 사람은 더 이상 내려받을 수 없습니다." — 되돌리기는 없다. 쓰던 보관 공간은 바로 비워진다.
|
||||
- **기간 지남 · 공유 멈춤**: 흐리게(이름 #667085 · 아이콘 #98A2B3 — 투명도가 아니라 글자색) + "기간 지남 — 11월 29일까지였습니다" · "공유 멈춤 — 보낸 사람이 링크를 끊었습니다", 내려받기 단추 없음.
|
||||
보낸 사람 자신에게는 받은 수가 남고 끊은 날이 보인다 — "1.2 GB · 3회 받음 · 공유 멈춤 — 10월 31일에 끊었습니다"(한국 날짜 · 응답의 `revokedAt`, 없으면 "공유 멈춤 — 링크를 끊었습니다").
|
||||
끝난 뒤 90일이 지나면 첨부 칸에서 빠지고 덩이는 그냥 본문이 된다.
|
||||
- **보안 검사**: 검사 중이면 [다운로드]를 `aria-disabled`(title · 까닭 연결) + "보안 검사 중 — 끝나면 받을 수 있습니다". 위험은 빨간 줄 "위험 — 보안 검사에서 막았습니다. 받을 수 없습니다"(단추 없음). 검사하지 못한 파일은 받을 수 있고 글로만 알린다.
|
||||
- **전달 쓰기 화면**: 원래 메일의 덩이는 인용 속 글(링크)로 그대로 간다 — 첨부 칸에 다시 싣지 않고 칸 아래 ⓘ 한 줄로만 알린다 "원래 메일의 대용량 첨부 2개는 인용 속 링크로 함께 갑니다 — 11월 29일까지".
|
||||
공유가 멈췄거나 기간이 지난 링크면 갈색 한 줄 "인용 속 ‘도면_초안.dwg’ 링크는 공유가 멈춰 받을 수 없습니다."("… 기간이 지나 받을 수 없습니다.", 파일이 여럿이면 "‘설계도_v3.dwg’ 외 1개") — 보내기는 막지 않는다.
|
||||
전달하는 사람이 새 큰 파일을 더하면 그것은 이 메일의 새 링크다(첨부 칸의 「링크로 보내는 파일」 — 전달하는 사람의 보관 한도에 잡힌다).
|
||||
받는 쪽에서 인용 속 원래 링크는 본문 글이고, 내려받기 화면의 보낸 사람 · 끊을 수 있는 사람은 처음 보낸 사람(링크 주인)이다. 답장도 같다.
|
||||
- 목록의 클립 아이콘은 지금처럼 메일에 담긴 첨부만 본다 — 링크 파일만 있는 메일은 클립이 없다(아래 "처음 판에서 미룬 것").
|
||||
|
||||
### 내려받기 화면 (`SharePage`)
|
||||
|
||||
- **독립 화면**: 앱 레일 · 상단 바 · 로그인 메뉴가 없다. 머리 바(#0B3640, 60)에 흰 ARA SYSTEM 로고(88) + "파일 내려받기", 바탕 #F6F7F9 에 가운데 열 640. 탭 제목 "김아라 님이 보낸 파일 — ARA SYSTEM".
|
||||
- **정상**: "김아라 님이 보낸 파일 2개 (1.8 GB)"(20) → "보낸 사람 김아라 <ara.kim@arasystem.it.kr> · ARA SYSTEM"(로그인한 계정의 값 — 보낸 사람이 바꿀 수 없다) →
|
||||
"2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간) · 30일 남음" → 파일 목록(줄마다 아이콘 36 · 이름 · "1.2 GB · 보안 검사 · 이상 없음" · [내려받기](채운 단추 44)) →
|
||||
"내려받다 끊기면 브라우저의 다운로드 목록에서 다시 시도하세요 — 받은 곳부터 이어서 받습니다." →
|
||||
청록 상자 "이 화면은 로그인이나 비밀번호를 묻지 않습니다. 묻는 화면이 나오면 닫고 보낸 사람에게 확인해 주세요." →
|
||||
"받을 이유가 없는 파일이면 postmaster@arasystem.it.kr 로 알려 주세요."(운영 설정 — 처음 값 postmaster@메일 도메인) → 영어 한 줄.
|
||||
- 메일 제목 · 본문 · 받는 사람, 내려받은 수 · 남은 수는 보이지 않는다. [내려받기]는 형식과 상관없이 늘 저장이고, 누르기 직전에 상태를 한 번 더 묻는다. noindex · no-referrer.
|
||||
- **상태**(모든 상태에서 머리 · 영어 한 줄은 그대로. 신고 주소 줄은 링크 정보를 받은 상태에서만 — 없는 링크 · 요청이 너무 많음 · 오류 응답에는 그 값이 없다):
|
||||
- 검사 중 — 모두 검사 중이면 제목 "보안 검사 중입니다" / "파일을 검사하고 있습니다. 몇 분 뒤 이 화면을 다시 열어 주세요." + [다시 확인]. 일부면 그 줄만 "보안 검사 중" + [내려받기] `aria-disabled`.
|
||||
- 위험 — "내려받을 수 없는 파일입니다" / "보안 검사에서 위험한 내용이 발견되어 막았습니다. 보낸 사람에게 알렸습니다." 그 줄은 빨강 "위험 — 막았습니다", 단추 없음.
|
||||
- 검사하지 못함 — 제목은 그대로, 줄에 갈색 "검사하지 못함 — 암호가 걸린 압축 파일 등 안을 볼 수 없는 파일입니다. 보낸 사람을 아는 경우에만 여세요." + [내려받기](받을 수 있다).
|
||||
- 횟수 초과(파일마다 운영 설정 — 처음 값 500번, 넘은 파일만) — 그 줄에 "내려받을 수 있는 횟수를 넘었습니다. 보낸 사람에게 다시 보내 달라고 요청해 주세요."(숫자는 쓰지 않는다 — 한도는 운영 설정이고 응답에 없다)
|
||||
- 만료 — "내려받을 수 있는 기간이 지났습니다" / "2026년 11월 29일까지 받을 수 있었던 파일입니다. 파일은 지워져 다시 받을 수 없습니다. 필요하면 보낸 사람(김아라, ara.kim@arasystem.it.kr)에게 다시 보내 달라고 요청해 주세요."
|
||||
- 회수 — "보낸 사람이 공유를 멈췄습니다" / "필요하면 보낸 사람(김아라, ara.kim@arasystem.it.kr)에게 문의해 주세요." 파일 이름도 보이지 않는다.
|
||||
- 없는 링크(모르는 주소 · 끝난 지 90일이 지나 기록을 지운 링크) — "찾을 수 없는 링크입니다" / "메일의 주소가 끝까지 복사되었는지 확인해 주세요." 보낸 사람도 보이지 않는다.
|
||||
- 잠시 막힘(같은 IP 의 요청이 너무 많음) — "요청이 너무 많습니다" / "잠시 뒤 다시 시도해 주세요." + [다시 시도].
|
||||
- **좁은 폭(휴대폰 360)**: 머리 52 · 로고 76, 보낸 사람 주소는 이름 아래 줄로, 줄마다 [내려받기]가 이름 아래 전체 폭(44).
|
||||
|
||||
### 설정 › 메일 › 대용량 첨부 (`SettingsLargeFiles`)
|
||||
|
||||
- **메뉴**: 메일 묶음의 차단한 주소 아래 「대용량 첨부」 — 서명 · 부재중 자동 응답 · 필터 · 보내기 · 읽기 · 차단한 주소 · 대용량 첨부 · 알림.
|
||||
- 머리 "대용량 첨부" + "메일에 링크로 보낸 큰 파일입니다. 받는 사람은 보낸 날부터 30일 동안 내려받고, 링크를 끊으면 바로 받을 수 없습니다."
|
||||
- **사용량 상자**: "보관 사용량 6.2 / 10 GB"(메일함 사용량과 같은 표기) + 4px 막대(90% 를 넘으면 빨강 — 메일함 사용량과 같다) +
|
||||
"보낸 링크 4.3 GB · 보내지 않은 파일 1.9 GB(임시 저장 · 올리다 멈춤). 기간이 지나거나, 링크를 끊거나, 보내지 않은 파일을 지우면 바로 줄어듭니다."
|
||||
- **보내지 않은 파일**(사용량 상자 바로 아래 — 공간을 바로 비울 수 있는 곳이라 보낸 링크보다 위): 「보내지 않은 파일 3개」 + "1.9 GB · 임시 저장한 메일에 실렸거나 올리다 멈춘 파일 — 지우면 바로 공간이 생깁니다". 없으면 묶음째 보이지 않는다.
|
||||
- 줄(52): 아이콘 · 이름 / (올리다 멈춘 파일은 앞에 갈색 "올리다 멈춤") · 크기(멈춘 파일은 "1.2 GB" / "420 MB 올림") ·
|
||||
실린 저장본의 제목 · 링크는 보이지 않는다(2026-10-30 — 저장본 id 는 저장할 때마다 바뀌고 엔진 밖에서도 지워져 서버가 따라가기 무겁다, 계약 4절 `LargeFile` 에 없음) ·
|
||||
남는 기한 "11월 29일까지 남음" / "올린 뒤 30일"(올리다 멈춘 파일은 이어 올리기 규칙대로 "내일 오후 2:41까지 남음" / "마지막으로 올린 뒤 24시간") · [지우기](36, 빨간 글).
|
||||
- [지우기] → 확인 창(alertdialog 400) "보내지 않은 파일을 지울까요?" / "‘시공도면_v4.dwg’(620 MB) 파일을 지웁니다. 이 파일을 실은 임시 저장본이 있으면 열 때 첨부 칸에 ‘지운 파일’로 보입니다. 지운 파일은 되돌릴 수 없습니다."
|
||||
[지우기](빨강) [취소](처음 초점 · Esc) → "‘시공도면_v4.dwg’ 파일을 지웠습니다." — 줄이 사라지고 사용량이 바로 준다.
|
||||
그 저장본을 쓰기 화면에서 열면 첨부 칸에 빨간 줄 "설정 › 대용량 첨부에서 지운 파일입니다 — 빼고 다시 올려 주세요."(쓰기 화면의 "기간이 지나 지운 파일"과 같은 모양 · 다른 글).
|
||||
- **보낸 링크**: 「보낸 링크 6개」 + "메일마다 한 덩이 · 최근에 보낸 차례". 메일(링크)마다 한 덩이 — 링크가 메일 한 통에 하나라 [링크 끊기]는 덩이에 한 번이다(파일 줄마다 두면 같은 메일의 다른 파일까지 끊긴다).
|
||||
- 머리: 메일 제목(청록 링크 → 보낸편지함의 그 메일, 메일을 지웠으면 글자만) / "10월 30일 (금) 보냄 · 파일 2개 · 1.8 GB" · 오른쪽 "11월 29일까지" / "30일 남음"(3일 이하면 갈색 굵게) · [링크 끊기](36).
|
||||
- 파일 줄(44 · 바탕 #FBFBFC): 아이콘 · 이름 · 크기 · "3회 받음" / "마지막 오늘 오후 2:41"(받지 않았으면 "아직 받지 않음") · 상태.
|
||||
- 상태 글: 사용 중 · 검사 중(회색) · 위험(빨강 굵게 + "내려받기를 막음", 줄 바탕 #FEF3F2) · 기간 지남 · 공유 멈춤. 검사하지 못한 파일은 상태 아래 갈색 "검사하지 못함". 뱃지 없이 글자만.
|
||||
- 끝난 링크는 덩이째 흐리게 — 머리 오른쪽 "공유 멈춤 / 10월 22일에 끊음" · "기간 지남 / 10월 25일까지였습니다", 단추 없음. 90일 뒤 목록에서 빠진다.
|
||||
- 최근에 보낸 차례 · 20개씩 [더 보기].
|
||||
- **[링크 복사]는 두지 않는다** — 서버는 링크를 해시로만 알아 원래 주소를 모른다. 주소는 보낸 메일의 읽기 화면(보낸 사람 자신)에서 복사한다.
|
||||
- [링크 끊기] → 읽기 화면과 같은 확인 창 · 같은 알림(창 글에 메일 제목). 끊는 동안 그 덩이의 [링크 끊기]를 막고, 끝나면 초점을 덩이 머리로.
|
||||
- **빈 상태**: 사용량 "0 / 10 GB" + "링크로 보낸 파일이 없습니다" / "메일 쓰기에서 20MB 가 넘는 파일을 고르면 링크로 보냅니다. 받는 사람은 보낸 날부터 30일 동안 내려받을 수 있습니다."
|
||||
보내지 않은 파일이 있으면 그 묶음은 위에 그대로 있다. 목록을 받지 못하면 지금의 목록 오류와 같은 모양.
|
||||
- **위험이 나오면** 빨간 알림 "올린 대용량 파일 ‘견적_요청서.zip’에서 위험한 내용이 발견되어 막았습니다." + [대용량 첨부 보기] —
|
||||
웹메일을 열어 둔 동안 실시간 알림(`large-file`)으로 바로 띄운다. 그때 열려 있지 않았으면 알림은 없고 목록 · 첨부 칸의 상태(위험)로만 안다 —
|
||||
이벤트에 보냈는지가 없어 "보낸 파일 · 내려받기를 막았습니다"로 나누지 않는다(계약 4절 "악성 검사"의 문구). 같은 메일의 다른 파일은 그대로 받을 수 있다.
|
||||
|
||||
### 처음 판에서 미룬 것
|
||||
|
||||
- 링크 → 메일에 담기 되돌리기 단추(제품 조사 3.1) — 빼고 다시 고르면 한도 안에서 메일에 담긴다.
|
||||
- 링크 파일만 있는 메일의 목록 클립 아이콘 — 목록의 클립은 지금처럼 메일에 담긴 첨부만 본다.
|
||||
- 전달 원본 · 저장본에서 이어 온 첨부의 「링크로 보내기」 — 이 쓰기 화면에서 고른 파일에만.
|
||||
- 확정본이 미룬 것: 기간 고르기 · 연장, 다시 보내기, 받는 사람 확인 코드 · 비밀번호, 사내만, 받는 사람별 링크 · 기록, 처음 받음 · 만료 전 알림, 미리 보기, zip 모두 받기,
|
||||
폴더 올리기, 관리자 화면 · 퇴사자 처리, 보낸 메일을 지울 때 파일도, 모바일 앱, 파일 저장소 연동.
|
||||
|
||||
## 구현하며 정한 보완 (2026-10-26 — 캔버스에는 아직 없다)
|
||||
|
||||
- 찾지 못한 첨부(보관 시간이 지난 업로드·지워진 원본)는 실패로 표시하고, 전달 원본에는 "빼기"만 둔다(다시 올릴 수 없다).
|
||||
@@ -801,3 +940,25 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
엔진 규칙이 꺼져 있으면(`isActive: false`) 머리 아래 노란 줄(단추 없음) "지금은 차단이 동작하지 않습니다. 메일 서버의 규칙이 다른 곳에서 바뀌었습니다 — 주소를 차단하거나 풀면 다시 켜집니다." —
|
||||
차단 · 풀기가 성공하면 켜진 것으로 본다(서버가 다시 켠다 — 계약 2절).
|
||||
- **새 토큰 · 아이콘**: `--color-on-brand-track` · `--color-on-brand-progress` · `--color-on-brand-divider` · `--color-warning-border`, 아이콘 printer · clock · block(원 + 빗금).
|
||||
|
||||
### 11단계 구현하며 정한 보완 (2026-10-30 — 캔버스에는 아직 없다)
|
||||
|
||||
데이터 · 값은 계약 4절 "올리기 › 클라이언트"의 "구현하며 정함"이 같은 내용이다.
|
||||
|
||||
- **올리기 차례**: 고르자마자 서버에 올리기를 모두 만든다(임시 저장본에 바로 실려 나가기 확인 글이 그대로 맞다). 조각은 파일 3개까지 함께 보내고, 나머지 줄은 오른쪽에 "차례를 기다리는 중"(처음 몇 초의 "시간 계산 중" 자리).
|
||||
- **끝내기**: 조각을 다 보냈으면 끝내기를 기다리는 동안 오른쪽 글은 "마무리하는 중"이다. 끝내기가 오래 진행 중이면 2 · 5 · 15 · 30초 간격으로 다시 묻고, 1분 넘게 그대로면 "오래 끊김"처럼 멈추고 [다시 시도].
|
||||
- **서버가 올리기를 잊었을 때(404)**: 멈춤 + "서버에서 올리던 파일을 찾을 수 없습니다 — 다시 시도하면 처음부터 올립니다." — [다시 시도]를 누를 때만 처음부터 올린다(사전 `RES_001` 문구로는 [다시 시도]의 뜻을 알 수 없어 화면 글로 둔다).
|
||||
고른 파일이 없는 줄(저장본에서 이어 온 것)이나 보내기의 `MAIL_016` 뒤 다시 물어 404 면 "서버에서 찾을 수 없는 파일입니다 — 빼고 다시 올려 주세요."
|
||||
- **서버 거절 문구**: 서버가 준 코드(`FILE_001` · `FILE_002` · `FILE_005` · `FILE_006` · `VAL_001` · `FILE_003`)는 사전 문구다. 위 "올리기 전 검사"의 화면 글은 서버에 묻기 전(크기 · 개수 · 보관량)에만 쓴다 —
|
||||
"지금은 서버에 공간이 모자라 …"는 쓰지 않고 `FILE_006` 사전 문구.
|
||||
- **「링크로 보내기」**: 대용량 사전 검사(개수 · 보관량)를 먼저 보고 통과할 때만 일반 첨부에서 뺀다. 걸리면 첨부는 그대로 두고 그 글을 오류 알림으로(보관량이면 + [대용량 첨부 보기]).
|
||||
- **다시 고르기**: 이름 · 크기 · 고친 시각이 다르면 줄 안에 알리고 기다린다(보드). 같은데 맨 앞 조각의 SHA-256 이 다르거나 서버가 맨 앞 조각을 아직 못 받았으면 앞 올리기를 지우고 새로 올린다(계약).
|
||||
`crypto.subtle` 이 없는 곳(https · localhost 밖)에서는 지문 없이 보내고 다시 고르기는 늘 새 올리기다 — 운영은 https 라 해당 없다.
|
||||
- **인용 경계**: 다시 연 답장 · 전달 저장본(보내기 취소로 되돌린 초안 포함)은 "맨 위 인용의 첫 자식이 ‘원본 메일’ 머리 문단 + 머리 표"로 원문 인용을 알아본다 —
|
||||
그래야 덩이가 인용 위에 들어가고, 저장본에서 돌지 않던 인용 줄이기도 함께 돈다(2026-10-30 리뷰).
|
||||
- **검사 상태 다시 묻기**: 쓰기 화면 · 읽기 화면은 검사 중인 파일이 있는 동안 15 · 30 · 60초 간격으로 최대 30번, 내려받기 화면은 20번. 읽기 화면은 막힌 [다운로드]를 누르면 한 번 바로 묻고,
|
||||
메일을 바꾸거나 닫거나 세션이 바뀌면 멈춘다.
|
||||
- **보내기 직전 인용 줄이기**: 서버가 넣을 덩이 몫(파일 수 · 이름 길이로 어림)을 더해 잰다.
|
||||
- **설정 › 대용량 첨부**: 보낸 링크 머리의 메일 제목이 없으면(지웠거나 읽지 못함 — `subject: null`) 글자만, 빈 제목은 메일이 있는 것이라 링크다.
|
||||
[지우기]가 `FILE_003`(그사이 보내져 링크에 실림)이면 목록 · 사용량을 다시 받고 사전 문구로 알린다.
|
||||
- **내려받기 화면**: 보낸 사람 이름이 비어 있으면 주소를 쓴다. 불러오는 동안은 `role="status"` "불러오는 중"(자리 막대는 `aria-hidden`).
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>설정 · 대용량 첨부</title>
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<style>
|
||||
@font-face{font-family:'Pretendard';src:url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2-variations'),url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2');font-weight:45 920;font-style:normal;font-display:swap}
|
||||
body{margin:0}
|
||||
a{color:#0E7C86}a:hover{color:#0A5F67}
|
||||
:focus-visible{outline:2px solid #0E7C86;outline-offset:2px}
|
||||
.primary{background:#0E7C86;color:#FFFFFF}
|
||||
.primary:hover{background:#0A5F67}
|
||||
.ghost{background:transparent;color:#4A5363}
|
||||
.ghost:hover{background:#F2F4F7;color:#171C26}
|
||||
.danger-solid{background:#B42318;color:#FFFFFF}
|
||||
.danger-solid:hover{background:#912018}
|
||||
.nav-item{color:#344054}
|
||||
.nav-item:hover{background:#F2F4F7}
|
||||
.line-btn{background:#FFFFFF;color:#344054;border:1px solid #D0D5DD}
|
||||
.line-btn:hover{background:#F2F4F7}
|
||||
.line-danger{background:#FFFFFF;color:#B42318;border:1px solid #D0D5DD}
|
||||
.line-danger:hover{background:#FEF3F2}
|
||||
.subject-link{color:#0A5F67;text-decoration:none}
|
||||
.subject-link:hover{text-decoration:underline}
|
||||
.toast-action{background:transparent;color:#9ED6DB}
|
||||
.toast-action:hover{background:rgba(255,255,255,0.1)}
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="width: 1440px; height: 2200px; box-sizing: border-box; padding: 48px; display: flex; flex-direction: column; gap: 28px; background: #F6F7F9; color: #171C26; font-family: Pretendard, -apple-system, 'Apple SD Gothic Neo', 'Malgun Gothic', system-ui, sans-serif; font-size: 13px; line-height: 1.5; word-break: keep-all; overflow-wrap: anywhere">
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 6px; max-width: 1060px">
|
||||
<h1 style="margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.02em">설정 · 대용량 첨부</h1>
|
||||
<p style="margin: 0; font-size: 13.5px; color: #4A5363">링크로 보낸 큰 파일과 아직 보내지 않은 파일을 한곳에서 봅니다 — 보관 사용량, 끝나는 날, 받은 수를 보고 링크를 끊거나 보내지 않은 파일을 지웁니다. 메뉴는 메일 묶음의 「대용량 첨부」(차단한 주소 아래). 초안 — 사용자 확인 전.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 220px 800px minmax(0, 1fr); gap: 20px; align-items: start">
|
||||
|
||||
<nav aria-label="설정" style="display: flex; flex-direction: column; gap: 2px; padding: 12px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<span style="padding: 6px 10px 2px; font-size: 12px; font-weight: 600; color: #667085">계정</span>
|
||||
<a href="#" class="nav-item" style="display: flex; align-items: center; min-height: 40px; padding: 0 10px; border-radius: 8px; text-decoration: none">비밀번호</a>
|
||||
<span style="padding: 10px 10px 2px; font-size: 12px; font-weight: 600; color: #667085">메일</span>
|
||||
<a href="#" class="nav-item" style="display: flex; align-items: center; min-height: 40px; padding: 0 10px; border-radius: 8px; text-decoration: none">서명</a>
|
||||
<a href="#" class="nav-item" style="display: flex; align-items: center; min-height: 40px; padding: 0 10px; border-radius: 8px; text-decoration: none">부재중 자동 응답</a>
|
||||
<a href="#" class="nav-item" style="display: flex; align-items: center; min-height: 40px; padding: 0 10px; border-radius: 8px; text-decoration: none">필터</a>
|
||||
<a href="#" class="nav-item" style="display: flex; align-items: center; min-height: 40px; padding: 0 10px; border-radius: 8px; text-decoration: none">보내기 · 읽기</a>
|
||||
<a href="#" class="nav-item" style="display: flex; align-items: center; min-height: 40px; padding: 0 10px; border-radius: 8px; text-decoration: none">차단한 주소</a>
|
||||
<a href="#" aria-current="page" style="display: flex; align-items: center; min-height: 40px; padding: 0 10px; border-radius: 8px; background: #E3F2F3; color: #0A5F67; font-weight: 600; text-decoration: none">대용량 첨부</a>
|
||||
<a href="#" class="nav-item" style="display: flex; align-items: center; min-height: 40px; padding: 0 10px; border-radius: 8px; text-decoration: none">알림</a>
|
||||
</nav>
|
||||
|
||||
<section aria-labelledby="lf-title" style="display: flex; flex-direction: column; gap: 22px; box-sizing: border-box; padding: 20px 32px 36px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 4px">
|
||||
<h2 id="lf-title" style="margin: 0; font-size: 18px; font-weight: 700; letter-spacing: -0.01em">대용량 첨부</h2>
|
||||
<p style="margin: 0; color: #4A5363">메일에 링크로 보낸 큰 파일입니다. 받는 사람은 보낸 날부터 30일 동안 내려받고, 링크를 끊으면 바로 받을 수 없습니다.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; padding: 14px 16px; border: 1px solid #EEF0F3; border-radius: 8px">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
|
||||
<span id="lf-usage-label" style="font-weight: 600">보관 사용량</span>
|
||||
<span style="font-weight: 600; font-variant-numeric: tabular-nums">6.2 / 10 GB</span>
|
||||
</div>
|
||||
<div role="meter" aria-labelledby="lf-usage-label" aria-valuemin="0" aria-valuemax="10" aria-valuenow="6.2" aria-valuetext="62%" style="height: 4px; border-radius: 2px; background: #E4E7EC"><div style="width: 62%; height: 4px; border-radius: 2px; background: #0E7C86"></div></div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">보낸 링크 4.3 GB · 보내지 않은 파일 1.9 GB(임시 저장 · 올리다 멈춤). 기간이 지나거나, 링크를 끊거나, 보내지 않은 파일을 지우면 바로 줄어듭니다.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 8px">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px">
|
||||
<h3 style="margin: 0; font-size: 14px; font-weight: 700">보내지 않은 파일 3개</h3>
|
||||
<span style="font-size: 12.5px; color: #667085">1.9 GB · 임시 저장한 메일에 실렸거나 올리다 멈춘 파일 — 지우면 바로 공간이 생깁니다</span>
|
||||
</div>
|
||||
<ul aria-label="보내지 않은 파일" style="list-style: none; margin: 0; padding: 0; border: 1px solid #EEF0F3; border-radius: 8px">
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 84px 176px auto; align-items: center; gap: 10px; min-height: 52px; padding: 4px 8px 4px 14px; border-bottom: 1px solid #EEF0F3">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">시공도면_v4.dwg</span><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #667085">저장본 <a href="#" class="subject-link">12월 정기 점검 일정 안내</a></span></span>
|
||||
<span style="font-size: 12px; color: #667085; text-align: right; font-variant-numeric: tabular-nums">620 MB</span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35; font-variant-numeric: tabular-nums"><span>11월 29일까지 남음</span><span style="font-size: 12px; color: #667085">올린 뒤 30일</span></span>
|
||||
<button type="button" class="line-danger" style="min-height: 36px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">지우기</button>
|
||||
</li>
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 84px 176px auto; align-items: center; gap: 10px; min-height: 52px; padding: 4px 8px 4px 14px; border-bottom: 1px solid #EEF0F3">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">현장영상_1030.mp4</span><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #7A5200">올리다 멈춤 · <span style="color: #667085">저장본 <a href="#" class="subject-link">11월 현장 점검 자료 2차</a></span></span></span>
|
||||
<span style="display: flex; flex-direction: column; align-items: flex-end; line-height: 1.35; font-size: 12px; color: #667085; font-variant-numeric: tabular-nums"><span>1.2 GB</span><span>420 MB 올림</span></span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35; font-variant-numeric: tabular-nums"><span>내일 오후 2:41까지 남음</span><span style="font-size: 12px; color: #667085">마지막으로 올린 뒤 24시간</span></span>
|
||||
<button type="button" class="line-danger" style="min-height: 36px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">지우기</button>
|
||||
</li>
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 84px 176px auto; align-items: center; gap: 10px; min-height: 52px; padding: 4px 8px 4px 14px">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="min-width: 0; display: flex; flex-direction: column; line-height: 1.35"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">발표자료_초안.pptx</span><span style="font-size: 12px; color: #667085">저장본 없음</span></span>
|
||||
<span style="font-size: 12px; color: #667085; text-align: right; font-variant-numeric: tabular-nums">64 MB</span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35; font-variant-numeric: tabular-nums"><span>11월 25일까지 남음</span><span style="font-size: 12px; color: #667085">올린 뒤 30일</span></span>
|
||||
<button type="button" class="line-danger" style="min-height: 36px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">지우기</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 8px">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px">
|
||||
<h3 style="margin: 0; font-size: 14px; font-weight: 700">보낸 링크 6개</h3>
|
||||
<span style="font-size: 12.5px; color: #667085">메일마다 한 덩이 · 최근에 보낸 차례</span>
|
||||
</div>
|
||||
<ul aria-label="링크로 보낸 메일" style="list-style: none; margin: 0; padding: 0; border: 1px solid #EEF0F3; border-radius: 8px">
|
||||
|
||||
<li style="display: flex; flex-direction: column; gap: 4px; padding: 10px 8px 10px 14px; border-bottom: 1px solid #EEF0F3">
|
||||
<div style="display: flex; align-items: center; gap: 12px">
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35">
|
||||
<a href="#" class="subject-link" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600">11월 현장 점검 자료 보내드립니다</a>
|
||||
<span style="font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">10월 30일 (금) 보냄 · 파일 2개 · 1.8 GB</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; line-height: 1.35; font-variant-numeric: tabular-nums">
|
||||
<span style="font-weight: 500">11월 29일까지</span>
|
||||
<span style="font-size: 12px; color: #667085">30일 남음</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; gap: 6px">
|
||||
<button type="button" class="line-danger" style="min-height: 36px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">링크 끊기</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px">
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 64px 160px 112px; align-items: center; gap: 10px; min-height: 44px; padding: 0 8px; border-radius: 6px; background: #FBFBFC">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">설계도_v3.dwg</span>
|
||||
<span style="font-size: 12px; color: #667085; text-align: right; font-variant-numeric: tabular-nums">1.2 GB</span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35; font-variant-numeric: tabular-nums"><span>3회 받음</span><span style="font-size: 12px; color: #667085">마지막 오늘 오후 2:41</span></span>
|
||||
<span style="color: #344054">사용 중</span>
|
||||
</li>
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 64px 160px 112px; align-items: center; gap: 10px; min-height: 44px; padding: 0 8px; border-radius: 6px; background: #FBFBFC">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">현장영상_1029.mp4</span>
|
||||
<span style="font-size: 12px; color: #667085; text-align: right; font-variant-numeric: tabular-nums">612 MB</span>
|
||||
<span style="color: #667085">아직 받지 않음</span>
|
||||
<span style="color: #667085">검사 중</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li style="display: flex; flex-direction: column; gap: 4px; padding: 10px 8px 10px 14px; border-bottom: 1px solid #EEF0F3">
|
||||
<div style="display: flex; align-items: center; gap: 12px">
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35">
|
||||
<a href="#" class="subject-link" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600">견적 요청서 회신드립니다</a>
|
||||
<span style="font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">10월 28일 (수) 보냄 · 파일 1개 · 24 MB</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; line-height: 1.35; font-variant-numeric: tabular-nums">
|
||||
<span style="font-weight: 500">11월 27일까지</span>
|
||||
<span style="font-size: 12px; color: #667085">28일 남음</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; gap: 6px">
|
||||
<button type="button" class="line-danger" style="min-height: 36px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">링크 끊기</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px">
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 64px 160px 112px; align-items: center; gap: 10px; min-height: 44px; padding: 0 8px; border-radius: 6px; background: #FEF3F2">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">견적_요청서.zip</span>
|
||||
<span style="font-size: 12px; color: #667085; text-align: right; font-variant-numeric: tabular-nums">24 MB</span>
|
||||
<span style="color: #667085">아직 받지 않음</span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35"><span style="font-weight: 600; color: #B42318">위험</span><span style="font-size: 12px; color: #B42318">내려받기를 막음</span></span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li style="display: flex; flex-direction: column; gap: 4px; padding: 10px 8px 10px 14px; border-bottom: 1px solid #EEF0F3">
|
||||
<div style="display: flex; align-items: center; gap: 12px">
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35">
|
||||
<a href="#" class="subject-link" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600">협력사 자료 공유드립니다</a>
|
||||
<span style="font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">10월 27일 (화) 보냄 · 파일 1개 · 860 MB</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; line-height: 1.35; font-variant-numeric: tabular-nums">
|
||||
<span style="font-weight: 500">11월 26일까지</span>
|
||||
<span style="font-size: 12px; color: #667085">27일 남음</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; gap: 6px">
|
||||
<button type="button" class="line-danger" style="min-height: 36px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">링크 끊기</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px">
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 64px 160px 112px; align-items: center; gap: 10px; min-height: 44px; padding: 0 8px; border-radius: 6px; background: #FBFBFC">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">협력사_자료모음.7z</span>
|
||||
<span style="font-size: 12px; color: #667085; text-align: right; font-variant-numeric: tabular-nums">860 MB</span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35; font-variant-numeric: tabular-nums"><span>2회 받음</span><span style="font-size: 12px; color: #667085">마지막 10월 28일 오전 9:30</span></span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35"><span style="color: #344054">사용 중</span><span style="font-size: 12px; color: #7A5200">검사하지 못함</span></span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li style="display: flex; flex-direction: column; gap: 4px; padding: 10px 8px 10px 14px; border-bottom: 1px solid #EEF0F3">
|
||||
<div style="display: flex; align-items: center; gap: 12px">
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35">
|
||||
<a href="#" class="subject-link" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600">홍보 영상 최종본 전달드립니다</a>
|
||||
<span style="font-size: 12px; color: #667085; font-variant-numeric: tabular-nums">10월 2일 (금) 보냄 · 파일 1개 · 1.6 GB</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; line-height: 1.35; font-variant-numeric: tabular-nums">
|
||||
<span style="font-weight: 500">11월 1일까지</span>
|
||||
<span style="font-size: 12px; font-weight: 600; color: #7A5200">2일 남음</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; gap: 6px">
|
||||
<button type="button" class="line-danger" style="min-height: 36px; padding: 0 12px; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">링크 끊기</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px">
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 64px 160px 112px; align-items: center; gap: 10px; min-height: 44px; padding: 0 8px; border-radius: 6px; background: #FBFBFC">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">홍보영상_최종.mov</span>
|
||||
<span style="font-size: 12px; color: #667085; text-align: right; font-variant-numeric: tabular-nums">1.6 GB</span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35; font-variant-numeric: tabular-nums"><span>12회 받음</span><span style="font-size: 12px; color: #667085">마지막 10월 29일 오전 11:05</span></span>
|
||||
<span style="color: #344054">사용 중</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li style="display: flex; flex-direction: column; gap: 4px; padding: 10px 8px 10px 14px; border-bottom: 1px solid #EEF0F3; color: #667085">
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 36px">
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35">
|
||||
<a href="#" class="subject-link" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: #667085">도면 초안 검토 부탁드립니다</a>
|
||||
<span style="font-size: 12px; font-variant-numeric: tabular-nums">10월 20일 (화) 보냄 · 파일 1개 · 340 MB</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; line-height: 1.35; font-variant-numeric: tabular-nums">
|
||||
<span style="font-weight: 600">공유 멈춤</span>
|
||||
<span style="font-size: 12px">10월 22일에 끊음</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px">
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 64px 160px 112px; align-items: center; gap: 10px; min-height: 44px; padding: 0 8px; border-radius: 6px; background: #FBFBFC">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#98A2B3" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">도면_초안.dwg</span>
|
||||
<span style="font-size: 12px; text-align: right; font-variant-numeric: tabular-nums">340 MB</span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35; font-variant-numeric: tabular-nums"><span>1회 받음</span><span style="font-size: 12px">마지막 10월 21일 오후 3:20</span></span>
|
||||
<span>공유 멈춤</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li style="display: flex; flex-direction: column; gap: 4px; padding: 10px 8px 10px 14px; color: #667085">
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 36px">
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.35">
|
||||
<a href="#" class="subject-link" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: #667085">행사 사진 원본입니다</a>
|
||||
<span style="font-size: 12px; font-variant-numeric: tabular-nums">9월 25일 (금) 보냄 · 파일 1개 · 1.1 GB</span>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; line-height: 1.35; font-variant-numeric: tabular-nums">
|
||||
<span style="font-weight: 600">기간 지남</span>
|
||||
<span style="font-size: 12px">10월 25일까지였습니다</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px">
|
||||
<li style="display: grid; grid-template-columns: 16px minmax(0, 1fr) 64px 160px 112px; align-items: center; gap: 10px; min-height: 44px; padding: 0 8px; border-radius: 6px; background: #FBFBFC">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#98A2B3" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500">행사사진_원본.zip</span>
|
||||
<span style="font-size: 12px; text-align: right; font-variant-numeric: tabular-nums">1.1 GB</span>
|
||||
<span style="display: flex; flex-direction: column; line-height: 1.35; font-variant-numeric: tabular-nums"><span>7회 받음</span><span style="font-size: 12px">마지막 10월 19일 오전 10:12</span></span>
|
||||
<span>기간 지남</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px">
|
||||
<button type="button" class="line-btn" style="min-height: 44px; padding: 0 16px; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer">더 보기</button>
|
||||
<span style="font-size: 12.5px; color: #667085">최근에 보낸 차례 · 20개씩. 끝난 링크는 90일 뒤 목록에서 빠집니다.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 20px">
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">줄 만드는 법</h2>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>위 「보내지 않은 파일」 — 임시 저장한 메일에 실렸거나 올리다 멈춘 파일. 줄마다 이름 · 실린 저장본(없으면 "저장본 없음") · 크기 · 남는 기한 · [지우기]. 없으면 묶음째 보이지 않습니다. 공간을 바로 비울 수 있는 곳이라 위에 둡니다</li>
|
||||
<li>아래 「보낸 링크」 — 메일(링크)마다 한 덩이. 머리에 메일 제목 · 보낸 날 · 끝나는 날 · [링크 끊기], 아래에 파일 줄(이름 · 크기 · 받은 수와 마지막으로 받은 때 · 상태). 링크가 메일 한 통에 하나라 단추는 덩이에 한 번입니다</li>
|
||||
<li>상태 글: 사용 중 · 검사 중 · 위험(빨강) · 기간 지남 · 공유 멈춤(흐리게). 검사하지 못한 파일은 상태 아래 갈색 한 줄. 뱃지 없이 글자만</li>
|
||||
<li>남은 날이 3일 이하면 "n일 남음"을 갈색 굵게</li>
|
||||
<li>[링크 끊기] · [지우기] 36 — 차단한 주소의 [차단 풀기]와 같은 밀도 예외(새 토큰 없음). 끝난 링크에는 단추가 없습니다</li>
|
||||
<li>[링크 복사]는 두지 않습니다 — 서버는 링크를 해시로만 알아 원래 주소를 모릅니다. 주소는 보낸 메일의 읽기 화면에서 복사합니다</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">누르면</h2>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>메일 제목 → 보낸편지함의 그 메일. 메일을 지웠으면 제목은 글자만(링크 아님)</li>
|
||||
<li>저장본 제목 → 쓰기 화면에서 그 저장본을 이어 씁니다</li>
|
||||
<li>[링크 끊기] → 확인 창(아래) → 그 덩이는 "공유 멈춤"으로 바뀌고 사용량이 바로 줄어듭니다</li>
|
||||
<li>[지우기] → 확인 창(아래) → 줄이 사라지고 사용량이 바로 줄어듭니다. 그 파일을 싣고 있던 저장본을 열면 첨부 칸에 "지운 파일"</li>
|
||||
<li>받은 수는 처음부터 받기 시작한 수 — 화면 열기 · 이어 받기 · 내가 받은 것은 빼고. 누가 받았는지는 남기지 않습니다</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; align-items: start">
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">링크 끊기 확인 창</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">모달 400 · 되돌릴 수 없는 일 — 처음 초점 「취소」</p>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding: 20px 12px; border-radius: 10px; background: rgba(16, 24, 28, 0.08)">
|
||||
<div role="alertdialog" aria-modal="true" aria-labelledby="lf-cut-title" aria-describedby="lf-cut-message" style="width: 100%; max-width: 400px; box-sizing: border-box; padding: 18px 20px 14px; border: 1px solid #E6E8EC; border-radius: 12px; background: #FFFFFF; box-shadow: 0 14px 36px rgba(16, 24, 28, 0.14); display: flex; flex-direction: column; gap: 10px">
|
||||
<h3 id="lf-cut-title" style="margin: 0; font-size: 15px; font-weight: 700">대용량 첨부 링크를 끊을까요?</h3>
|
||||
<p id="lf-cut-message" style="margin: 0; color: #4A5363">‘11월 현장 점검 자료 보내드립니다’의 파일 2개를 받는 사람 모두 더는 내려받을 수 없습니다. 이미 받은 파일은 되돌릴 수 없고, 끊은 링크는 다시 열 수 없습니다.</p>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px">
|
||||
<button type="button" class="danger-solid" style="min-height: 44px; padding: 0 16px; border: 0; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer">링크 끊기</button>
|
||||
<button type="button" class="ghost" style="min-height: 44px; padding: 0 14px; border: 0; border-radius: 8px; font: inherit; font-weight: 500; cursor: pointer; outline: 2px solid #0E7C86; outline-offset: 2px">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="status" style="display: flex; align-items: center; gap: 10px; min-height: 44px; box-sizing: border-box; padding: 6px 14px; border-radius: 10px; background: #0B3640; color: #FFFFFF; box-shadow: 0 10px 28px rgba(16, 24, 28, 0.22)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9FE0B5" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; font-weight: 500">링크를 끊었습니다. 받는 사람은 더 이상 내려받을 수 없습니다.</span>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">읽기 화면의 [링크 끊기]와 같은 창 · 같은 알림입니다(되돌리기 단추 없음). 끊는 동안 그 덩이의 [링크 끊기]를 막고, 끝나면 초점을 덩이 머리로 옮깁니다.</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">보내지 않은 파일 지우기 확인 창</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">모달 400 · 되돌릴 수 없는 일 — 처음 초점 「취소」</p>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding: 20px 12px; border-radius: 10px; background: rgba(16, 24, 28, 0.08)">
|
||||
<div role="alertdialog" aria-modal="true" aria-labelledby="lf-del-title" aria-describedby="lf-del-message" style="width: 100%; max-width: 400px; box-sizing: border-box; padding: 18px 20px 14px; border: 1px solid #E6E8EC; border-radius: 12px; background: #FFFFFF; box-shadow: 0 14px 36px rgba(16, 24, 28, 0.14); display: flex; flex-direction: column; gap: 10px">
|
||||
<h3 id="lf-del-title" style="margin: 0; font-size: 15px; font-weight: 700">보내지 않은 파일을 지울까요?</h3>
|
||||
<p id="lf-del-message" style="margin: 0; color: #4A5363">‘시공도면_v4.dwg’(620 MB) 파일을 지웁니다. 이 파일을 실은 임시 저장본 「12월 정기 점검 일정 안내」를 열면 첨부 칸에 ‘지운 파일’로 보입니다. 지운 파일은 되돌릴 수 없습니다.</p>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px">
|
||||
<button type="button" class="danger-solid" style="min-height: 44px; padding: 0 16px; border: 0; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer">지우기</button>
|
||||
<button type="button" class="ghost" style="min-height: 44px; padding: 0 14px; border: 0; border-radius: 8px; font: inherit; font-weight: 500; cursor: pointer; outline: 2px solid #0E7C86; outline-offset: 2px">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="status" style="display: flex; align-items: center; gap: 10px; min-height: 44px; box-sizing: border-box; padding: 6px 14px; border-radius: 10px; background: #0B3640; color: #FFFFFF; box-shadow: 0 10px 28px rgba(16, 24, 28, 0.22)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9FE0B5" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
<span style="flex: 1 1 auto; font-weight: 500">‘시공도면_v4.dwg’ 파일을 지웠습니다.</span>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">지우면 줄이 사라지고 사용량이 바로 줄어듭니다(되돌리기 없음). 그 저장본을 쓰기 화면에서 열면 첨부 칸에 빨간 줄 "설정 › 대용량 첨부에서 지운 파일입니다 — 빼고 다시 올려 주세요."(쓰기 보드의 "기간이 지나 지운 파일"과 같은 모양). 저장본이 없으면 둘째 문장을 뺍니다. 올리다 멈춘 파일도 같은 창입니다 — 지우면 이어 올릴 수 없습니다.</p>
|
||||
</section>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 20px">
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">빈 상태</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">링크로 보낸 메일이 하나도 없을 때 · 사용량은 그대로, 보내지 않은 파일이 있으면 그 묶음도 위에 그대로</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border: 1px solid #EEF0F3; border-radius: 8px">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
|
||||
<span style="font-weight: 600">보관 사용량</span>
|
||||
<span style="font-weight: 600; font-variant-numeric: tabular-nums">0 / 10 GB</span>
|
||||
</div>
|
||||
<div aria-hidden="true" style="height: 4px; border-radius: 2px; background: #E4E7EC"></div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 26px 16px; border: 1px solid #EEF0F3; border-radius: 10px; background: #FBFBFC; text-align: center">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#98A2B3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<p style="margin: 4px 0 0; font-weight: 600">링크로 보낸 파일이 없습니다</p>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #667085">메일 쓰기에서 20MB 가 넘는 파일을 고르면 링크로 보냅니다. 받는 사람은 보낸 날부터 30일 동안 내려받을 수 있습니다.</p>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">목록을 받지 못하면 지금의 목록 오류와 같은 모양 — "메일 서버와 연결할 수 없습니다 …" + [다시 시도].</p>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">보낸 뒤 위험이 나왔을 때</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">오른쪽 아래 알림 — 목록의 빨간 줄은 그대로 남는다</p>
|
||||
</div>
|
||||
<div role="alert" style="display: flex; align-items: center; gap: 10px; min-height: 44px; box-sizing: border-box; padding: 6px 6px 6px 14px; border-radius: 10px; background: #912018; color: #FFFFFF; box-shadow: 0 10px 28px rgba(16, 24, 28, 0.22)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FDA29B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
<span style="flex: 1 1 auto; font-weight: 500">보낸 파일 ‘견적_요청서.zip’에서 위험한 내용이 발견되어 내려받기를 막았습니다.</span>
|
||||
<a href="#" class="toast-action" style="flex: 0 0 auto; display: inline-flex; align-items: center; min-height: 36px; padding: 0 10px; border-radius: 6px; color: #FFD8D3; font-weight: 600; text-decoration: none">대용량 첨부 보기</a>
|
||||
</div>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>위험 알림은 웹메일을 열어 둔 동안이면 바로(실시간 알림의 길), 아니면 다음에 메일 화면에 들어올 때 한 번. 단추가 있으니 10초 — 마우스 · 초점이 있으면 멈춤(지금 알림 규칙)</li>
|
||||
<li>같은 메일의 다른 파일은 그대로 받을 수 있습니다 — 링크를 끊을지는 보낸 사람이 정합니다</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":1440,"height":2200}}'>
|
||||
class Component extends DCLogic {
|
||||
renderVals() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,287 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>내려받기 화면 — files.arasystem.it.kr</title>
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<style>
|
||||
@font-face{font-family:'Pretendard';src:url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2-variations'),url('/_blob/16067b9aa5137abf808e3dbef30c42e0') format('woff2');font-weight:45 920;font-style:normal;font-display:swap}
|
||||
body{margin:0}
|
||||
a{color:#0E7C86}a:hover{color:#0A5F67}
|
||||
:focus-visible{outline:2px solid #0E7C86;outline-offset:2px}
|
||||
.primary{background:#0E7C86;color:#FFFFFF}
|
||||
.primary:hover{background:#0A5F67}
|
||||
.ghost{background:transparent;color:#4A5363}
|
||||
.ghost:hover{background:#F2F4F7;color:#171C26}
|
||||
.line-btn{background:#FFFFFF;color:#0A5F67;border:1px solid #D0D5DD}
|
||||
.line-btn:hover{background:#F2F4F7}
|
||||
</style>
|
||||
</helmet>
|
||||
<div style="width: 1440px; height: 2130px; box-sizing: border-box; padding: 48px; display: flex; flex-direction: column; gap: 28px; background: #F6F7F9; color: #171C26; font-family: Pretendard, -apple-system, 'Apple SD Gothic Neo', 'Malgun Gothic', system-ui, sans-serif; font-size: 13px; line-height: 1.5; word-break: keep-all; overflow-wrap: anywhere">
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 6px; max-width: 1060px">
|
||||
<h1 style="margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.02em">내려받기 화면 — files.arasystem.it.kr/s/…</h1>
|
||||
<p style="margin: 0; font-size: 13.5px; color: #4A5363">메일 속 링크는 파일이 아니라 이 화면으로 옵니다. 로그인 없이 링크가 있는 누구나 열고, 계정 비밀번호는 절대 묻지 않습니다. 앱 레일 · 로그인 메뉴가 없는 독립 화면이며, 메일 제목 · 본문 · 받는 사람은 보이지 않습니다. 초안 — 사용자 확인 전.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 900px minmax(0, 1fr); gap: 20px; align-items: start">
|
||||
|
||||
<section aria-label="내려받기 화면 — 정상" style="display: flex; flex-direction: column; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px; overflow: hidden">
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 42px; padding: 0 14px; background: #EEF0F3; border-bottom: 1px solid #E6E8EC">
|
||||
<span aria-hidden="true" style="display: inline-flex; gap: 6px"><span style="width: 10px; height: 10px; border-radius: 50%; background: #D0D5DD"></span><span style="width: 10px; height: 10px; border-radius: 50%; background: #D0D5DD"></span><span style="width: 10px; height: 10px; border-radius: 50%; background: #D0D5DD"></span></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 12px; border-radius: 6px; background: #FFFFFF; color: #344054; font-size: 12.5px">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#4A5363" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"><strong style="font-weight: 600; color: #171C26">files.arasystem.it.kr</strong>/s/Qx7mK2vR9pL4wT8nB3cF6hJ1dS5gA0eY2uI7oP9kZ3x</span>
|
||||
</div>
|
||||
<span style="flex: 0 0 auto; font-size: 12px; color: #667085">탭 제목 "김아라 님이 보낸 파일 — ARA SYSTEM"</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; background: #F6F7F9">
|
||||
<header style="display: flex; align-items: center; gap: 14px; min-height: 60px; padding: 0 28px; background: #0B3640">
|
||||
<img src="/_blob/3a2f4115a8f90207faa7d3a51cd9aec2" alt="ARA SYSTEM" width="88" height="42" style="display: block; width: 88px; height: auto">
|
||||
<span aria-hidden="true" style="width: 1px; height: 18px; background: rgba(255, 255, 255, 0.25)"></span>
|
||||
<span style="color: #E6EFF0; font-size: 13px; font-weight: 600">파일 내려받기</span>
|
||||
</header>
|
||||
|
||||
<main style="display: flex; justify-content: center; padding: 32px 24px 24px">
|
||||
<div style="width: 100%; max-width: 640px; display: flex; flex-direction: column; gap: 16px">
|
||||
<div style="display: flex; flex-direction: column; gap: 6px">
|
||||
<h2 style="margin: 0; font-size: 20px; line-height: 1.4; font-weight: 650; letter-spacing: -0.015em">김아라 님이 보낸 파일 2개 (1.8 GB)</h2>
|
||||
<p style="margin: 0; color: #344054">보낸 사람 <strong style="font-weight: 600">김아라</strong> <ara.kim@arasystem.it.kr> · ARA SYSTEM</p>
|
||||
<p style="margin: 0; color: #344054"><strong style="font-weight: 600">2026년 11월 29일(일)까지</strong> 내려받을 수 있습니다 (한국 시간) · 30일 남음</p>
|
||||
</div>
|
||||
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; border: 1px solid #E6E8EC; border-radius: 10px; background: #FFFFFF">
|
||||
<li style="display: flex; align-items: center; gap: 12px; padding: 12px 12px 12px 14px; border-bottom: 1px solid #EEF0F3">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 8px; background: #F2F4F7; color: #4A5363"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.4"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 600">설계도_v3.dwg</span><span style="font-size: 12.5px; color: #667085">1.2 GB · 보안 검사 · 이상 없음</span></div>
|
||||
<a href="#" class="primary" aria-label="설계도_v3.dwg 내려받기 (1.2 GB)" style="flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 16px; border-radius: 8px; font-weight: 600; text-decoration: none"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>내려받기</a>
|
||||
</li>
|
||||
<li style="display: flex; align-items: center; gap: 12px; padding: 12px 12px 12px 14px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 8px; background: #F2F4F7; color: #4A5363"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.4"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 600">현장영상_1029.mp4</span><span style="font-size: 12.5px; color: #667085">612 MB · 보안 검사 · 이상 없음</span></div>
|
||||
<a href="#" class="primary" aria-label="현장영상_1029.mp4 내려받기 (612 MB)" style="flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 16px; border-radius: 8px; font-weight: 600; text-decoration: none"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>내려받기</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p style="margin: -6px 0 0; font-size: 12.5px; color: #667085">내려받다 끊기면 브라우저의 다운로드 목록에서 다시 시도하세요 — 받은 곳부터 이어서 받습니다.</p>
|
||||
|
||||
<div style="display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px; border-radius: 8px; background: #E3F2F3; color: #0A5F67">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto; margin-top: 2px"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
<p style="margin: 0; font-weight: 500">이 화면은 로그인이나 비밀번호를 묻지 않습니다. 묻는 화면이 나오면 닫고 보낸 사람에게 확인해 주세요.</p>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12.5px; color: #4A5363">받을 이유가 없는 파일이면 <a href="mailto:postmaster@arasystem.it.kr">postmaster@arasystem.it.kr</a> 로 알려 주세요.</p>
|
||||
<p lang="en" style="margin: 0; font-size: 12px; color: #667085">Files from 김아라 (ara.kim@arasystem.it.kr) — download by Nov 29, 2026 (KST). This page never asks for a login or password.</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer style="padding: 8px 24px 20px; text-align: center; font-size: 12px; color: #98A2B3">ARA SYSTEM · files.arasystem.it.kr</footer>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 20px">
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">넣는 것</h2>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>회사 로고 · 이름 — 모양을 늘 같게("평소와 다르면 의심"이 통하게)</li>
|
||||
<li>보낸 사람 이름 · 주소 — 로그인한 계정의 값이라 보낸 사람이 바꿀 수 없습니다. "someone" 같은 말은 쓰지 않습니다</li>
|
||||
<li>끝나는 날(한국 시간) · n일 남음</li>
|
||||
<li>파일마다 이름 · 크기 · 검사 상태 · [내려받기]. 묶음 크기는 제목에</li>
|
||||
<li>로그인 · 비밀번호를 묻지 않는다는 안내(청록 상자), 신고 · 문의 주소(운영 설정 — 처음 값 postmaster@메일 도메인), 영어 한 줄</li>
|
||||
</ul>
|
||||
<h2 style="margin: 6px 0 0; font-size: 14px; font-weight: 700">넣지 않는 것</h2>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>메일 제목 · 본문 · 받는 사람 — 링크가 전달되면 그 사람도 보고, 숨은참조가 드러납니다</li>
|
||||
<li>내려받은 수 · 남은 수 — 상한(파일당 500회)은 넘었을 때만 알립니다</li>
|
||||
<li>로그인 칸 · 광고 · 다른 서비스 안내 · 앱 레일 · 상단 바</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">동작</h2>
|
||||
<ul style="margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; color: #344054">
|
||||
<li>[내려받기]는 파일을 바로 받습니다 — 형식과 상관없이 늘 저장(브라우저가 열지 않음). 누르기 직전에 상태를 한 번 더 물어, 그사이 끝났으면 화면에서 알립니다</li>
|
||||
<li>세는 것은 처음부터 받기 시작한 수뿐 — 화면 열기 · 이어 받기 · 보안 장비가 링크를 미리 여는 것은 세지 않습니다</li>
|
||||
<li>검색 엔진에 걸리지 않고(noindex), 이 화면에서 나가는 요청에 주소가 실리지 않습니다(no-referrer)</li>
|
||||
<li>폭 640 의 가운데 열. 좁으면 줄마다 단추가 이름 아래로 내려갑니다(아래 휴대폰)</li>
|
||||
<li>모든 상태에서 머리(로고) · 신고 주소 · 영어 한 줄은 그대로입니다</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 400px minmax(0, 1fr); gap: 20px; align-items: start">
|
||||
|
||||
<section aria-label="좁은 폭 — 휴대폰 360px" style="display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="align-self: stretch; display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">좁은 폭 — 휴대폰 360px</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">메일 앱의 링크로 열었을 때 · 단추는 줄 아래 전체 폭</p>
|
||||
</div>
|
||||
<div style="width: 360px; box-sizing: border-box; border: 1px solid #D0D5DD; border-radius: 20px; overflow: hidden; background: #F6F7F9">
|
||||
<header style="display: flex; align-items: center; gap: 10px; min-height: 52px; padding: 0 16px; background: #0B3640">
|
||||
<img src="/_blob/3a2f4115a8f90207faa7d3a51cd9aec2" alt="ARA SYSTEM" width="76" height="36" style="display: block; width: 76px; height: auto">
|
||||
<span aria-hidden="true" style="width: 1px; height: 14px; background: rgba(255, 255, 255, 0.25)"></span>
|
||||
<span style="color: #E6EFF0; font-size: 12.5px; font-weight: 600">파일 내려받기</span>
|
||||
</header>
|
||||
<div style="display: flex; flex-direction: column; gap: 14px; padding: 20px 16px 18px">
|
||||
<div style="display: flex; flex-direction: column; gap: 4px">
|
||||
<h3 style="margin: 0; font-size: 18px; line-height: 1.4; font-weight: 650">김아라 님이 보낸 파일 2개 (1.8 GB)</h3>
|
||||
<p style="margin: 0; color: #344054">보낸 사람 <strong style="font-weight: 600">김아라</strong><br>ara.kim@arasystem.it.kr</p>
|
||||
<p style="margin: 0; color: #344054"><strong style="font-weight: 600">2026년 11월 29일(일)까지</strong> 내려받을 수 있습니다 (한국 시간) · 30일 남음</p>
|
||||
</div>
|
||||
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; border: 1px solid #E6E8EC; border-radius: 10px; background: #FFFFFF">
|
||||
<li style="display: flex; flex-direction: column; gap: 10px; padding: 12px; border-bottom: 1px solid #EEF0F3">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.4"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600">설계도_v3.dwg</span><span style="font-size: 12px; color: #667085">1.2 GB · 보안 검사 · 이상 없음</span></div>
|
||||
</div>
|
||||
<a href="#" class="primary" style="display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 44px; border-radius: 8px; font-weight: 600; text-decoration: none"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>내려받기</a>
|
||||
</li>
|
||||
<li style="display: flex; flex-direction: column; gap: 10px; padding: 12px">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<span aria-hidden="true" style="flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; background: #F2F4F7; color: #4A5363"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></span>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.4"><span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600">현장영상_1029.mp4</span><span style="font-size: 12px; color: #667085">612 MB · 보안 검사 · 이상 없음</span></div>
|
||||
</div>
|
||||
<a href="#" class="primary" style="display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 44px; border-radius: 8px; font-weight: 600; text-decoration: none"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>내려받기</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p style="margin: -4px 0 0; font-size: 12px; color: #667085">내려받다 끊기면 브라우저의 다운로드 목록에서 다시 시도하세요 — 이어서 받습니다.</p>
|
||||
<div style="display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; border-radius: 8px; background: #E3F2F3; color: #0A5F67; font-size: 12.5px">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto; margin-top: 2px"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
<p style="margin: 0; font-weight: 500">이 화면은 로그인이나 비밀번호를 묻지 않습니다. 묻는 화면이 나오면 닫고 보낸 사람에게 확인해 주세요.</p>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 12px; color: #4A5363">받을 이유가 없는 파일이면 <a href="mailto:postmaster@arasystem.it.kr">postmaster@arasystem.it.kr</a> 로 알려 주세요.</p>
|
||||
<p lang="en" style="margin: 0; font-size: 11.5px; color: #667085">Files from 김아라 (ara.kim@arasystem.it.kr) — download by Nov 29, 2026 (KST). This page never asks for a login or password.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; align-items: start">
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">검사 중</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">모든 파일이 검사 중일 때의 제목 · 일부면 그 줄만</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid #EEF0F3; border-radius: 10px; background: #F6F7F9">
|
||||
<h3 style="margin: 0; font-size: 17px; font-weight: 650">보안 검사 중입니다</h3>
|
||||
<p style="margin: 0; color: #344054">파일을 검사하고 있습니다. 몇 분 뒤 이 화면을 다시 열어 주세요.</p>
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 10px 10px 10px 12px; border: 1px solid #E6E8EC; border-radius: 8px; background: #FFFFFF">
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.4"><span style="font-weight: 600">현장영상_1029.mp4</span><span id="sp-scan-note" style="font-size: 12px; color: #667085">612 MB · 보안 검사 중</span></div>
|
||||
<a role="link" aria-disabled="true" aria-describedby="sp-scan-note" style="flex: 0 0 auto; display: inline-flex; align-items: center; min-height: 44px; padding: 0 14px; border-radius: 8px; background: #D0D5DD; color: #FFFFFF; font-weight: 600; cursor: not-allowed">내려받기</a>
|
||||
</div>
|
||||
<button type="button" class="line-btn" style="align-self: flex-start; min-height: 44px; padding: 0 14px; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer">다시 확인</button>
|
||||
<p lang="en" style="margin: 0; font-size: 12px; color: #667085">Security scan in progress — please check again in a few minutes.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">위험 — 막음</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">보안 검사에서 위험이 나온 파일 · 보낸 사람에게 알림</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid #EEF0F3; border-radius: 10px; background: #F6F7F9">
|
||||
<h3 style="margin: 0; font-size: 17px; font-weight: 650">내려받을 수 없는 파일입니다</h3>
|
||||
<p style="margin: 0; color: #344054">보안 검사에서 위험한 내용이 발견되어 막았습니다. 보낸 사람에게 알렸습니다.</p>
|
||||
<div style="display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 10px 12px; border: 1px solid #FBD7D3; border-radius: 8px; background: #FEF3F2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B42318" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex: 0 0 auto"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.4"><span style="font-weight: 600; color: #B42318">견적_요청서.zip</span><span style="font-size: 12px; color: #B42318">24 MB · 위험 — 막았습니다</span></div>
|
||||
</div>
|
||||
<p lang="en" style="margin: 0; font-size: 12px; color: #667085">This file was blocked by a security scan.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">검사하지 못함 — 받을 수는 있음</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">암호 압축처럼 안을 볼 수 없는 파일 · 제목은 그대로</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid #EEF0F3; border-radius: 10px; background: #F6F7F9">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 10px 10px 10px 12px; border: 1px solid #E6E8EC; border-radius: 8px; background: #FFFFFF">
|
||||
<div style="flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.4"><span style="font-weight: 600">협력사_자료모음.7z</span><span style="font-size: 12px; color: #7A5200">860 MB · 검사하지 못함 — 암호가 걸린 압축 파일입니다. 보낸 사람을 아는 경우에만 여세요.</span></div>
|
||||
<a href="#" class="primary" style="flex: 0 0 auto; display: inline-flex; align-items: center; min-height: 44px; padding: 0 14px; border-radius: 8px; font-weight: 600; text-decoration: none">내려받기</a>
|
||||
</div>
|
||||
<p lang="en" style="margin: 0; font-size: 12px; color: #667085">Not scanned (password-protected archive) — open it only if you know the sender.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">횟수 초과</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">파일마다 500번 — 넘은 파일만, 그 줄에서 알립니다</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid #EEF0F3; border-radius: 10px; background: #F6F7F9">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border: 1px solid #E6E8EC; border-radius: 8px; background: #FFFFFF">
|
||||
<span style="font-weight: 600; color: #667085">홍보영상_최종.mov</span>
|
||||
<span style="font-size: 12.5px; color: #344054">내려받을 수 있는 횟수를 넘었습니다 — 이 파일은 500번까지 받을 수 있습니다. 보낸 사람에게 다시 보내 달라고 요청해 주세요.</span>
|
||||
</div>
|
||||
<p lang="en" style="margin: 0; font-size: 12px; color: #667085">Download limit reached for this file — please ask the sender to share it again.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">만료</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">끝나는 날이 지났다 · 파일은 지웠다</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid #EEF0F3; border-radius: 10px; background: #F6F7F9">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#98A2B3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
||||
<h3 style="margin: 0; font-size: 17px; font-weight: 650">내려받을 수 있는 기간이 지났습니다</h3>
|
||||
<p style="margin: 0; color: #344054">2026년 11월 29일까지 받을 수 있었던 파일입니다. 파일은 지워져 다시 받을 수 없습니다. 필요하면 보낸 사람(김아라, ara.kim@arasystem.it.kr)에게 다시 보내 달라고 요청해 주세요.</p>
|
||||
<p lang="en" style="margin: 0; font-size: 12px; color: #667085">This link expired on Nov 29, 2026 (KST).</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">회수 — 공유 멈춤</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">보낸 사람이 링크를 끊었다 · 파일 이름도 보이지 않는다</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid #EEF0F3; border-radius: 10px; background: #F6F7F9">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#98A2B3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line></svg>
|
||||
<h3 style="margin: 0; font-size: 17px; font-weight: 650">보낸 사람이 공유를 멈췄습니다</h3>
|
||||
<p style="margin: 0; color: #344054">필요하면 보낸 사람(김아라, ara.kim@arasystem.it.kr)에게 문의해 주세요.</p>
|
||||
<p lang="en" style="margin: 0; font-size: 12px; color: #667085">The sender stopped sharing these files.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">없는 링크</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">모르는 주소 · 끝난 지 90일이 지나 기록을 지운 링크 — 보낸 사람도 보이지 않는다</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid #EEF0F3; border-radius: 10px; background: #F6F7F9">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#98A2B3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
<h3 style="margin: 0; font-size: 17px; font-weight: 650">찾을 수 없는 링크입니다</h3>
|
||||
<p style="margin: 0; color: #344054">메일의 주소가 끝까지 복사되었는지 확인해 주세요.</p>
|
||||
<p lang="en" style="margin: 0; font-size: 12px; color: #667085">Link not found — check that the whole address was copied.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="display: flex; flex-direction: column; gap: 10px; padding: 20px; background: #FFFFFF; border: 1px solid #E6E8EC; border-radius: 12px">
|
||||
<div style="display: flex; flex-direction: column; gap: 2px">
|
||||
<h2 style="margin: 0; font-size: 14px; font-weight: 700">잠시 막힘</h2>
|
||||
<p style="margin: 0; font-size: 12px; color: #667085">같은 곳(IP)에서 요청이 너무 많을 때 · 화면 열기 · 내려받기 모두</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid #EEF0F3; border-radius: 10px; background: #F6F7F9">
|
||||
<h3 style="margin: 0; font-size: 17px; font-weight: 650">요청이 너무 많습니다</h3>
|
||||
<p style="margin: 0; color: #344054">잠시 뒤 다시 시도해 주세요.</p>
|
||||
<button type="button" class="line-btn" style="align-self: flex-start; min-height: 44px; padding: 0 14px; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer">다시 시도</button>
|
||||
<p lang="en" style="margin: 0; font-size: 12px; color: #667085">Too many requests — please try again shortly.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props='{"$preview":{"width":1440,"height":2130}}'>
|
||||
class Component extends DCLogic {
|
||||
renderVals() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -33,9 +33,14 @@
|
||||
"MailReadTools.dc.html": { "x": 0, "y": 16820, "w": 1440, "h": 1220, "title": "읽기 화면 — 외부 이미지 · 인쇄 · 보낸 사람 차단" },
|
||||
"MailPrint.dc.html": { "x": 1520, "y": 16820, "w": 1440, "h": 1060, "title": "인쇄 화면" },
|
||||
"MailSendLater.dc.html": { "x": 3040, "y": 16820, "w": 1440, "h": 1500, "title": "보내기 취소 · 예약 발송" },
|
||||
"SettingsSendRead.dc.html": { "x": 4560, "y": 16820, "w": 1440, "h": 1000, "title": "설정 · 보내기 · 읽기 · 차단한 주소" }
|
||||
"SettingsSendRead.dc.html": { "x": 4560, "y": 16820, "w": 1440, "h": 1000, "title": "설정 · 보내기 · 읽기 · 차단한 주소" },
|
||||
"ComposeLargeFiles.dc.html": { "x": 0, "y": 18920, "w": 1440, "h": 2360, "title": "메일 쓰기 · 대용량 첨부" },
|
||||
"LargeFileBlock.dc.html": { "x": 1520, "y": 18920, "w": 1440, "h": 2020, "title": "본문 덩이 — 바깥 메일 프로그램에서" },
|
||||
"MailReadLargeFiles.dc.html": { "x": 3040, "y": 18920, "w": 1440, "h": 1940, "title": "읽기 화면 · 대용량 첨부" },
|
||||
"SharePage.dc.html": { "x": 4560, "y": 18920, "w": 1440, "h": 2130, "title": "내려받기 화면 (files.arasystem.it.kr)" },
|
||||
"SettingsLargeFiles.dc.html": { "x": 6080, "y": 18920, "w": 1440, "h": 2200, "title": "설정 · 대용량 첨부" }
|
||||
},
|
||||
"order": ["Main.dc.html", "Login.dc.html", "Compose.dc.html", "States.dc.html", "MainList.dc.html", "MainListRead.dc.html", "ComposeEditor.dc.html", "Settings.dc.html", "ComposeAssist.dc.html", "Search.dc.html", "BulkSelect.dc.html", "ListStates.dc.html", "SettingsAccount.dc.html", "SettingsVacation.dc.html", "MailboxManage.dc.html", "MailActions4.dc.html", "SettingsNotify.dc.html", "MailRealtime.dc.html", "SettingsFilter.dc.html", "MailFilterStates.dc.html", "MailFilterApply.dc.html", "MailFilterApplyStates.dc.html", "Contacts.dc.html", "ContactsEdit.dc.html", "ContactsMail.dc.html", "MailReadTools.dc.html", "MailPrint.dc.html", "MailSendLater.dc.html", "SettingsSendRead.dc.html"],
|
||||
"order": ["Main.dc.html", "Login.dc.html", "Compose.dc.html", "States.dc.html", "MainList.dc.html", "MainListRead.dc.html", "ComposeEditor.dc.html", "Settings.dc.html", "ComposeAssist.dc.html", "Search.dc.html", "BulkSelect.dc.html", "ListStates.dc.html", "SettingsAccount.dc.html", "SettingsVacation.dc.html", "MailboxManage.dc.html", "MailActions4.dc.html", "SettingsNotify.dc.html", "MailRealtime.dc.html", "SettingsFilter.dc.html", "MailFilterStates.dc.html", "MailFilterApply.dc.html", "MailFilterApplyStates.dc.html", "Contacts.dc.html", "ContactsEdit.dc.html", "ContactsMail.dc.html", "MailReadTools.dc.html", "MailPrint.dc.html", "MailSendLater.dc.html", "SettingsSendRead.dc.html", "ComposeLargeFiles.dc.html", "LargeFileBlock.dc.html", "MailReadLargeFiles.dc.html", "SharePage.dc.html", "SettingsLargeFiles.dc.html"],
|
||||
"notes": {
|
||||
"flow": { "x": 0, "y": -300, "text": "arasystem 웹메일 — 1단계 화면 (로그인 → 메일함 → 쓰기)", "kind": "title1", "maxW": 4480 },
|
||||
"phase2": { "x": 0, "y": 2180, "text": "2단계 화면 — 임시 저장 · 받는 사람 자동 완성 · 서명 (2026-10-27 확정)", "kind": "title1", "maxW": 2960 },
|
||||
@@ -45,7 +50,8 @@
|
||||
"phase6": { "x": 0, "y": 10380, "text": "6단계 화면 — 메일 필터 (설정 › 메일 › 필터 · 읽기 화면 ⋯ 「이런 메일로 필터 만들기」) — 2026-10-28 확정", "kind": "title1", "maxW": 2960 },
|
||||
"phase7": { "x": 0, "y": 12400, "text": "7단계 화면 — 이미 받은 메일에 필터 적용 · 필터 창 나가기 확인 · 부재중 안내 — 2026-10-29 확정", "kind": "title1", "maxW": 2960 },
|
||||
"phase8": { "x": 0, "y": 14280, "text": "8단계 화면 — 주소록 (내 연락처 · 그룹 · 사내 목록 · 메일에서 쓰기) — 2026-10-29 확정", "kind": "title1", "maxW": 4480 },
|
||||
"phase9": { "x": 0, "y": 16520, "text": "9단계 화면 — 보내기 취소 · 예약 발송 · 외부 이미지 보기 · 인쇄 · 보낸 사람 차단 — 2026-10-29 확정", "kind": "title1", "maxW": 6000 }
|
||||
"phase9": { "x": 0, "y": 16520, "text": "9단계 화면 — 보내기 취소 · 예약 발송 · 외부 이미지 보기 · 인쇄 · 보낸 사람 차단 — 2026-10-29 확정", "kind": "title1", "maxW": 6000 },
|
||||
"phase11": { "x": 0, "y": 18620, "text": "11단계 화면 — 대용량 첨부 (쓰기 · 본문 덩이 · 읽기 · 내려받기 화면 · 설정) — 초안, 사용자 확인 전", "kind": "title1", "maxW": 7520 }
|
||||
},
|
||||
"designSystems": []
|
||||
}
|
||||
|
||||
+684
-3
@@ -9,10 +9,13 @@
|
||||
- 성공 `{ "success": true, "data": … }` / 실패 `{ "success": false, "error": { "code", "message", "traceId" } }`
|
||||
- 날짜·시각은 ISO 8601 UTC 문자열, 목록은 `{ items, pageInfo }`
|
||||
- 이 단계에서 쓰는 에러 코드: `AUTH_001` `AUTH_002`(클라이언트 전용) `AUTH_003` `AUTH_004` `AUTH_005` `VAL_001` `RES_001` `REQ_001` `FILE_001` `FILE_002` `MAIL_001` `MAIL_002` `MAIL_003` `MAIL_004` `MAIL_005` `MAIL_006` `MAIL_007` `MAIL_008` `MAIL_009` `MAIL_010` `MAIL_011` `MAIL_012` `MAIL_013` `MAIL_014` `MAIL_015` `CONTACT_001` `CONTACT_002` `SYS_001`
|
||||
(11단계 — 4절 "대용량 첨부"가 `FILE_003` ~ `FILE_008` · `MAIL_016` 을 더한다)
|
||||
- **적용 범위: 1단계는 웹(브라우저) 전용이다.** 인증은 HttpOnly 쿠키뿐이고 토큰을 본문으로 주지 않는다.
|
||||
모바일 앱(Flutter)의 인증 방식(토큰 발급·`flutter_secure_storage`)은 앱 착수 때 이 문서에 추가한다.
|
||||
- **배포 형태: 화면과 API 는 같은 주소(origin)를 쓴다.** 운영은 프론트 nginx 가 `/api` 를 백엔드로 넘기고,
|
||||
개발은 vite 프록시가 넘긴다. 그래서 CORS 가 필요 없고 CSP `connect-src 'self'` 로 충분하다.
|
||||
- **경로는 대소문자를 가린다** — 이 문서의 경로는 모두 소문자이고, 대소문자 변형(`/API/…` · `/api/Messages/…` · `…/LARGE-FILES/…` · `/api/public-links/…/FILES/…`)은
|
||||
처리기에 닿지 않고 404 `RES_001` 이다(2026-10-30 — 앞단 nginx 의 받기 location 이 놓친 변형이 동시 연결 상한을 비켜 갔다, 4절 "운영 확인").
|
||||
- **출처 검사(CSRF)**: 상태를 바꾸는 요청(POST·PATCH·PUT·DELETE)은 브라우저가 붙이는 `Sec-Fetch-Site`·`Origin` 을 검사한다.
|
||||
다른 사이트에서 온 요청은 403 `AUTH_003`. 출처 정보가 아예 없는 요청(브라우저가 아닌 클라이언트)은 통과한다.
|
||||
- **프록시가 직접 내는 오류**: 프론트 nginx 가 백엔드에 닿기 전에 끝내는 요청도 표준 실패 형식으로 온다 —
|
||||
@@ -169,6 +172,7 @@ interface MessageDetail extends MessageSummary {
|
||||
isHtmlBodyOmitted: boolean // HTML 이 있었지만 정화 한도를 넘어 htmlBody 를 비웠으면 true (아래 "읽기 한도")
|
||||
isBodyTruncated: boolean // 보여 줄 본문(htmlBody — null 이거나 공백뿐이면 textBody)이 읽기 한도에서 잘렸으면 true
|
||||
attachments: MessageAttachment[]
|
||||
largeFiles: MessageLargeFile[] // 우리 서버가 넣은 대용량 첨부 덩이의 파일 — 링크 주인이 이 메일의 보낸 사람일 때만, 없으면 빈 배열 (11단계 — 4절 "우리 웹메일에서 읽기")
|
||||
}
|
||||
|
||||
interface Attachment { id: string; name: string; type: string; size: number }
|
||||
@@ -224,12 +228,13 @@ HTML 은 정화하기 **전에** 길이(HTML 부분이 여럿이면 이어 붙
|
||||
| `PATCH /api/messages` | `{ ids, isRead?, isFlagged?, mailboxId?, isJunk? }` — 여러 통 (아래 "여러 통 한꺼번에") | 200 `MessagesPatched` — 붙잡힌 메일(9단계)의 `mailboxId` · `isJunk` 는 바꾸지 않고 `failedIds` 로 | 400 `VAL_001`, 404 `RES_001`(없는 `mailboxId` — 아무것도 바꾸지 않음), 503 `MAIL_001` |
|
||||
| `DELETE /api/messages/{id}` | — | 204 — 휴지통이 아니면 휴지통으로 이동, 휴지통에만 있으면 영구 삭제(확인한 상태 그대로일 때만 — 아래). 휴지통과 다른 메일함에 함께 있으면 휴지통에서만 뺀다. **웹 클라이언트는 쓰지 않는다**(3단계 — 아래 "여러 통 한꺼번에") | 404 `RES_001`, 409 `MAIL_015`(붙잡힌 메일 — 9단계), 503 `MAIL_001`(확인하는 사이 계속 바뀜 — 지우지 않았다) |
|
||||
| `POST /api/message-deletions` | `{ ids }` — 휴지통에서 영구 삭제, 여러 통 (아래 "여러 통 한꺼번에") — 붙잡힌 메일(9단계)은 건너뛴다(`skippedIds`) | 200 `MessageDeletionResult` | 400 `VAL_001`, 503 `MAIL_001`(확인하는 사이 계속 바뀜 · 휴지통 없음 — 지우지 않았다) |
|
||||
| `POST /api/messages` | 아래 발송 본문 + `Idempotency-Key` 헤더 (예약은 `sendAt` — 9단계) | 201 `SentMessage` (보낸편지함의 메일 id + 받지 못한 사람 — 아래 "받는 사람별 결과" + 붙잡아 둔 발송 `pendingSend` — 9단계) | 400 `MAIL_002`·`MAIL_003`·`MAIL_004`·`MAIL_006`·`MAIL_007`·`MAIL_010`(받는 사람 모두 거절 — 없는 주소 등)·`VAL_001`, 403 `AUTH_003`(보낼 권한 없음), 409 `MAIL_005`(같은 키·다른 내용), 413 `FILE_002`(첨부 합계 초과 포함), 429 `REQ_001`(엔진 발송 한도·같은 키 처리 중·엔진 장애 직후 30초), 503 `MAIL_001` · `MAIL_011`(받는 사람 모두 지금은 받을 수 없음) |
|
||||
| `POST /api/messages` | 아래 발송 본문 + `Idempotency-Key` 헤더 (예약은 `sendAt` — 9단계) | 201 `SentMessage` (보낸편지함의 메일 id + 받지 못한 사람 — 아래 "받는 사람별 결과" + 붙잡아 둔 발송 `pendingSend` — 9단계) | 400 `MAIL_002`·`MAIL_003`·`MAIL_004`·`MAIL_006`·`MAIL_007`·`MAIL_010`(받는 사람 모두 거절 — 없는 주소 등)·`MAIL_016`(대용량 첨부 — 11단계, 4절)·`VAL_001`, 403 `AUTH_003`(보낼 권한 없음), 409 `MAIL_005`(같은 키·다른 내용), 413 `FILE_002`(첨부 합계 초과 포함), 429 `REQ_001`(엔진 발송 한도·같은 키 처리 중·엔진 장애 직후 30초), 503 `MAIL_001` · `MAIL_011`(받는 사람 모두 지금은 받을 수 없음) |
|
||||
| `POST /api/mail-attachments` | multipart `file` 1개 | 201 `Attachment` | 400 `FILE_001`, 413 `FILE_002` |
|
||||
| `GET /api/messages/{id}/attachments/{attachmentId}` | — | 파일 (표준 래퍼 없음, `Content-Disposition: attachment`) | 404 `RES_001` |
|
||||
| `GET`·`HEAD /api/messages/{id}/large-files/{fileId}` | `Range`(선택) | 그 메일의 대용량 첨부 파일 (표준 래퍼 없음, 200 · 206 · 416 — 11단계, 4절 "우리 웹메일에서 읽기") | 404 `RES_001`, 409 `FILE_008`(검사 중), 410 `FILE_007`(더 받을 수 없음), 503 `MAIL_001` |
|
||||
| `GET /api/messages/{id}/source` | — | 받은 그대로의 메일 `message/rfc822` (표준 래퍼 없음, `Content-Disposition: attachment; filename="<제목>.eml"`) — 4단계 | 404 `RES_001`, 503 `MAIL_001` |
|
||||
| `GET /api/drafts/{id}` | — | 200 `Draft` (아래 "임시 저장") | 404 `RES_001`(없음·초안이 아님), 503 `MAIL_001` |
|
||||
| `POST /api/drafts` | 초안 본문 | 201 `DraftSaved` | 400 `VAL_001`·`MAIL_002`·`MAIL_003`·`MAIL_004`·`MAIL_006`·`MAIL_007`·`FILE_001`, 413 `FILE_002`, 503 `MAIL_001` |
|
||||
| `POST /api/drafts` | 초안 본문 | 201 `DraftSaved` | 400 `VAL_001`·`MAIL_002`·`MAIL_003`·`MAIL_004`·`MAIL_006`·`MAIL_007`·`MAIL_016`(대용량 첨부 — 11단계, 4절)·`FILE_001`, 413 `FILE_002`, 503 `MAIL_001` |
|
||||
| `PUT /api/drafts/{id}` | 초안 본문 (전체 교체) | 200 `DraftSaved` — **새 id** | 위와 같음 + 404 `RES_001`(초안이 아닌 id — 지우지 않음) + 409 `MAIL_008`(다른 곳에서 이미 바뀜 — 저장 안 함) |
|
||||
| `GET /api/mail-signature` | — | 200 `MailSignature` (아래 "서명") | — |
|
||||
| `PUT /api/mail-signature` | `{ bodyHtml, body, isForNew, isForReply }` | 200 `MailSignature` | 400 `VAL_001` |
|
||||
@@ -314,7 +319,9 @@ interface MailQuota {
|
||||
"attachments": [{ "id": "<업로드 응답 id>", "name": "체크리스트.xlsx" }], // 선택, 20개 이하 (넘으면 MAIL_006)
|
||||
"inReplyTo": "<답장할 메일의 id>", // 선택 — 스레드 헤더를 서버가 채운다
|
||||
"draftId": "<이어 쓴 임시 저장본 id>", // 선택 — 보낸 뒤 서버가 그 초안을 지운다 (아래 "임시 저장")
|
||||
"sendAt": "2026-10-30T00:00:00.000Z" // 선택 — 예약 발송: 지금부터 5분 뒤 ~ 7일 안 (9단계 "보내기 취소 · 예약 발송")
|
||||
"sendAt": "2026-10-30T00:00:00.000Z", // 선택 — 예약 발송: 지금부터 5분 뒤 ~ 7일 안 (9단계 "보내기 취소 · 예약 발송")
|
||||
"largeFileIds": ["<올리기 id>"], // 선택 — 이 메일에 새로 싣는 내 대용량 파일(링크로 보낸다) (11단계 — 4절 "발송 · 임시 저장과 함께")
|
||||
"bodyQuoteStart": 1234 // 선택 — body 에서 인용이 시작하는 자리 = 덩이를 넣을 자리 (4절)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -438,6 +445,7 @@ interface RejectedRecipient {
|
||||
|
||||
### 첨부 업로드 한도
|
||||
|
||||
- 한도를 넘는 파일은 일반 첨부가 아니라 **대용량 첨부(링크)** 로 보낸다 — 4절(11단계). 아래 일반 첨부의 한도 · 흐름은 그대로다.
|
||||
- 파일당 `MAIL_ATTACHMENT_MAX_SIZE_MB` (기본 20MB) — 넘으면 413 `FILE_002`
|
||||
- 메일 한 통의 첨부 합계(전달하는 원본 첨부 포함, 같은 첨부를 두 번 붙이면 두 번 센다) `attachmentTotalMaxBytes`
|
||||
(`MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB`, 기본 25MB) — 넘으면 발송이 413 `FILE_002`.
|
||||
@@ -465,6 +473,7 @@ interface Draft {
|
||||
attachments: Attachment[] // 이 판의 첨부 — id 가 저장할 때마다 바뀐다
|
||||
inReplyTo: string | null // 답장할 메일 id (없거나 원 메일이 사라졌으면 null)
|
||||
updatedAt: string // 마지막으로 저장한 시각 (ISO 8601 UTC)
|
||||
largeFiles: (LargeFile | MissingLargeFile)[] // 대용량 첨부 — 내 파일(올리는 중 포함). id 는 저장해도 바뀌지 않는다 (11단계 — 4절 "임시 저장")
|
||||
}
|
||||
|
||||
interface DraftSaved { id: string; attachments: Attachment[]; updatedAt: string }
|
||||
@@ -789,6 +798,7 @@ data: {"reason":"token-expiring"}
|
||||
| `new-mail` | 받은편지함에 새로 배달된 메일이 있을 때 — `$draft` 아님 · `$seen` 아님 · 보낸 사람이 나 아님. 새것부터 최대 5통(`count` 는 전체 수). 같은 변화의 `state` 다음에 온다 |
|
||||
| `ping` | 25초마다(직원마다 한 간격 — 새 연결의 첫 ping 은 더 빨리 올 수 있다), 그리고 `upstream` 이 바뀌면 그때 바로. `upstream: false` 면 서버와 엔진 사이가 끊겨 다시 잇는 중이다 — 그동안 클라이언트는 1분 확인으로 돌아간다. 다시 잇는 중에 새로 연결하면 마지막으로 알던 `state` 와 `ping {upstream:false}` 가 바로 온다 |
|
||||
| `end` | 서버가 곧 닫는다 — `token-expiring`(액세스 토큰이 끝나기 30초 전 — 남은 시간이 30초 이하인 토큰(세션이 끝나기 30초 안에 갱신한 토큰)이면 토큰이 실제로 끝날 때) · `server-shutdown`(서버 종료 · 재시작) |
|
||||
| `large-file` | 내가 올린 대용량 파일이 악성 검사에서 위험으로 판정됐을 때 — `{"fileId","name","scanStatus":"infected"}` (11단계 — 4절 "악성 검사"). 지금 클라이언트는 모르는 이벤트를 버린다 |
|
||||
|
||||
| 연결 전 실패 (표준 실패 형식 JSON) | 코드 |
|
||||
| --- | --- |
|
||||
@@ -1369,3 +1379,674 @@ interface DirectoryEntry {
|
||||
상태에는 연 직원의 주소를 함께 싣고, 지금 로그인한 직원과 다르면 쓰지 않는다(같은 탭에서 다른 직원이 로그인한 뒤 뒤로 가기로 돌아온 경우 — 2026-10-29 리뷰).
|
||||
- **읽기 화면 ⋯**: 보낸 사람 주소가 내 주소록에 없으면 「보낸 사람을 주소록에 추가」(이름 · 주소를 채운 연락처 추가 창), 있으면 「주소록에서 보기」.
|
||||
판단은 메뉴를 열 때 `GET /api/contacts?q=<보낸 사람 주소>` 로 알림 없이 하고, 주소가 같은(대소문자 무시) 연락처가 있는지 본다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 대용량 첨부 — 11단계 (2026-10-30 확정)
|
||||
|
||||
> 2026-10-30 에 확정한 제품 · 기술 결정(사용자 "추천대로")과 디자인 보드를 그리며 정한 것(같은 날 — 이쪽이 앞선다)을 계약으로 옮겼고, 같은 날 사용자가 캔버스와 함께 확정했다(아래 "정한 것"의 추천도 그대로).
|
||||
> 백엔드 · 프론트가 이 절로 함께 만들고, 구현하며 바뀌면 이 절부터 고친다. "구현하며 정함"은 구현 때 값을 정해 여기에 적을 곳이다.
|
||||
> 화면 모양 · 글은 디자인 README "11단계"가 정한다 — 이 절은 데이터와 규칙만 다룬다.
|
||||
|
||||
일반 첨부 한도(파일 20MB · 합계 25MB — 2절 "첨부 업로드 한도")를 넘는 파일은 **메일 엔진이 아니라 우리 서버에** 올려 두고, 메일 본문에는 내려받기 링크를 넣는다(이하 "덩이").
|
||||
받는 사람은 로그인 없이 공개 화면 `<SHARE_PUBLIC_URL>/s/{token}` 에서 받는다. 보관은 보낸 날(한국)부터 30일째 날 끝까지다.
|
||||
|
||||
- **엔진에 두지 않는다** — 엔진(Stalwart 0.16.27)은 올리기 본문을 통째로 메모리에 받고(한 번 50,000,000바이트), 파일이 그 직원의 메일함 용량 · 메일 백업과 묶이며, 로그인 없는 링크가 없다(2026-10-30 개발 엔진 · 소스 확인).
|
||||
파일은 백엔드의 볼륨(`<PROJECT_NAME>_files` → `/app/files`), 기록은 PostgreSQL 이다. 새 의존성은 없다(백엔드 · 프론트 모두).
|
||||
- **개념 셋** — 파일(내용 · 주인 · 이름 · 크기 · 검사 결과), 공유 링크(메일 한 통의 파일 묶음 하나 — 토큰 · 끝나는 때 · 끊김), 쓰임(임시 저장본 · 보낸 메일). 2부 파일 저장소가 같은 파일 · 링크 · 올리기 API 를 쓴다.
|
||||
- **링크는 메일 한 통에 하나다** — 그 메일에 새로 실은 내 파일의 묶음이다. 보낸 사본과 받는 사람의 사본은 같은 메일이라(어댑터가 메일 하나를 만들어 같은 요청에서 제출한다 — `jmap-mail.provider.ts:2042-2075`)
|
||||
받는 사람별 링크 · 기록은 없고, **끊기도 링크(메일) 단위**다.
|
||||
- **답장 · 전달의 인용 속 원래 링크는 글(링크)로 그대로 간다** — 서버가 다시 싣거나 우리 링크로 바꾸지 않는다. 전달하는 사람이 새 대용량 파일을 더하면 그 메일의 링크는 하나(전달하는 사람 것)다.
|
||||
- 공개 경로는 로그인이 없다 — 바깥 거래처의 브라우저 · 내려받기 관리자 · 받는 쪽 보안 장비가 부른다(머리말의 "웹 전용" · 출처 검사는 로그인한 API 의 규칙이다). 모바일 앱은 이 단계 범위 밖이다.
|
||||
|
||||
### 공유 계약을 바꾸는 곳 (한데 모음)
|
||||
|
||||
| 곳 | 바뀌는 것 | 호환 |
|
||||
| --- | --- | --- |
|
||||
| 발송 `POST /api/messages` 본문 (2절) | `largeFileIds` · `bodyQuoteStart` 추가, 서식 본문의 인용 경계 표시 `<div data-ara-quote-start></div>` | 필드 추가 — 쓰지 않으면 지금과 같다(지문도 같다) |
|
||||
| 발송 실패 | 400 `MAIL_016` 추가. `MAIL_007` 은 덩이를 넣은 뒤에도 잰다 | 새 코드 |
|
||||
| 임시 저장 `POST`·`PUT /api/drafts` 본문 · `GET /api/drafts/{id}` 응답 (2절) | 같은 두 필드(올리는 중인 파일도 된다), 초안 머리글 `X-Ara-Large-Files`, `Draft.largeFiles`, 실패 `MAIL_016` | 필드 추가 |
|
||||
| 읽기 `GET /api/messages/{id}` (2절) | `MessageDetail.largeFiles`(링크 주인이 그 메일의 보낸 사람일 때만), `htmlBody` 안 그 덩이의 바깥 요소에 `data-ara-large-files` | 필드 · 속성 추가 — 지금 클라이언트는 `data-*` 를 지우고 덩이를 보통 본문으로 그린다 |
|
||||
| 받기 `GET`·`HEAD /api/messages/{id}/large-files/{fileId}` (2절 표에 한 줄) | 새 경로 — 우리 웹메일에서 대용량 첨부 받기 | 새 경로 |
|
||||
| 실시간 알림 `GET /api/mail-events` (2절) | `large-file` 이벤트(위험 판정) | 지금 클라이언트는 모르는 이벤트를 버린다(`frontend/src/shared/utils/mail-events.ts:97-110`) |
|
||||
| 이 절의 새 API | `/api/file-uploads` · `/api/file-quota` · `/api/share-links` · `/api/share-link-lookups` · `/api/public-links`, 화면 `/s/{token}` | 새 경로 — `/api/files` 는 템플릿 업로드 예제가 쓰고 있어 피했다 |
|
||||
| 에러 코드 | `FILE_003` ~ `FILE_008` · `MAIL_016` (아래 "에러 코드") | 레지스트리에 더하고 `npm run sync:error-codes` |
|
||||
|
||||
### 한도
|
||||
|
||||
| 한도 | 값(기본) | 넘으면 | 바꾸는 곳 |
|
||||
| --- | --- | --- | --- |
|
||||
| 한 파일 | 2,047MB (2,146,435,072바이트 — ClamAV 가 검사할 수 있는 2GiB 아래) | 413 `FILE_002`(만들 때) | `MAIL_LARGE_FILE_MAX_SIZE_MB` |
|
||||
| 형식 | 일반 첨부 허용 목록 + mp4 · mov · dwg · dxf · psd · ai · 7z — 실행 형식(exe · msi · bat · js · vbs · scr 등) · 디스크 이미지(iso · img · vhd)를 비롯해 그 밖은 받지 않는다 | 400 `FILE_001` | 상수 — 백엔드 정책과 프론트 상수가 짝(한쪽을 고치면 둘 다) |
|
||||
| 메일 한 통 | 대용량 파일 10개 — 그 메일에 새로 싣는 내 파일(인용 속 원래 링크는 세지 않는다). 일반 첨부(20개)와 따로 센다 | 400 `VAL_001` | `MAIL_LARGE_FILE_MAX_COUNT` |
|
||||
| 직원당 보관량 | 10GB (10,737,418,240바이트) — 올리는 중(선언한 크기) + 보내지 않은 파일 + 기간 안의 보낸 파일 | 409 `FILE_005`(만들 때) | `MAIL_LARGE_FILE_USER_QUOTA_GB` |
|
||||
| 직원당 보내지 않은 파일 수 | 50개(올리는 중 포함) | 409 `FILE_005` | 상수 |
|
||||
| 서버 총량 | 없음(비움) — 모든 직원의 보관량 합 | 507 `FILE_006`(만들 때) | `FILE_STORAGE_MAX_TOTAL_GB` |
|
||||
| 디스크 여유 | 만든 뒤에도 10GB 가 남아야 한다(진행 중인 올리기의 아직 받지 않은 바이트를 빼고) | 507 `FILE_006`(만들 때 · 쓰다 `ENOSPC`) | `FILE_STORAGE_MIN_FREE_GB` |
|
||||
| 조각 | 16MiB (16,777,216바이트) 고정 — 2,047MB 면 128개 | 400 `VAL_001`(길이) · 413 `FILE_002`(앞단 17MB) | 계약 상수 |
|
||||
| 내려받기 | 파일당 500회(받기 시작한 수 — 아래 "파일 응답"의 횟수) | 410 `FILE_007` | `SHARE_DOWNLOAD_LIMIT` |
|
||||
| 보관 | 보낸 날(한국)부터 30일째 날 끝 | 410 `FILE_007` | `MAIL_LARGE_FILE_RETENTION_DAYS` |
|
||||
|
||||
- MB · GB 는 1,048,576 · 1,073,741,824바이트다(일반 첨부 한도와 같은 셈 — `mail.settings.ts:74`). 한 파일 한도는 env 로 2,047MB 를 넘길 수 없다(검사기 한도 — 아래 "악성 검사").
|
||||
- 보관량 · 총량 · 여유는 **만들 때 선언한 크기로 미리 잡는다** — 조각을 보내는 동안에는 한도를 다시 보지 않는다. 같은 직원의 만들기는 하나씩 처리한다(동시에 둘이 한도를 넘지 않게).
|
||||
구현(2026-10-30): 만들기는 **서버 전체에서 하나씩**이다 — 서버 총량 · 디스크 여유를 함께 보므로 DB 의 트랜잭션 잠금(`pg_advisory_xact_lock`) 안에서 세고 만든다(인스턴스를 늘려도 그대로 맞다).
|
||||
- 기간이 지나거나 링크를 끊거나 보내지 않은 파일을 지우면 보관량은 정리 작업을 기다리지 않고 바로 줄어든다(요청 때 상태로 계산한다).
|
||||
|
||||
### 타입
|
||||
|
||||
```ts
|
||||
type ScanStatus = 'pending' | 'clean' | 'infected' | 'unscannable' // 검사 중 · 깨끗함 · 위험 · 검사하지 못함
|
||||
|
||||
/** 내가 올린 대용량 파일 — 올리기 API 의 응답 · 보내지 않은 파일 목록 · Draft.largeFiles */
|
||||
interface LargeFile {
|
||||
id: string // 올리기 id = 다 올린 뒤의 파일 id (바뀌지 않는다) — 발송 · 임시 저장의 largeFileIds 에 쓴다
|
||||
name: string // 서버가 정리한 이름 — 일반 첨부와 같은 정리(경로 · 제어 · 방향 제어 문자를 지우고 255자, 확장자는 남긴다 — attachment.policy.ts:220-239)
|
||||
size: number // 바이트
|
||||
status: 'uploading' | 'ready' // 올리는 중 · 다 올림
|
||||
chunkSize: number // 16,777,216 — 마지막 조각만 짧다
|
||||
chunkCount: number // ceil(size / chunkSize)
|
||||
receivedChunks: number[] // 받은 조각 번호(0 부터, 오름차순) — ready 면 모두
|
||||
firstChunkSha256: string | null // 받은 맨 앞 조각(0번)의 SHA-256 (base64) — 받기 전이면 null. 새로고침 뒤 다시 고른 파일이 같은지 맞춰 본다
|
||||
scanStatus: ScanStatus | null // ready 일 때만 (uploading 은 null)
|
||||
createdAt: string
|
||||
expiresAt: string // 이대로 두면 지워지는 때 — 아래 "기간 · 보관 · 정리"
|
||||
lastModified: number | null // 만들 때 받은 File.lastModified (ms)
|
||||
}
|
||||
|
||||
/** 임시 저장본이 가리키는데 이제 없는 파일 */
|
||||
interface MissingLargeFile {
|
||||
id: string
|
||||
status: 'missing'
|
||||
reason: 'deleted' | 'expired' // 지운 파일(설정 · 쓰기 화면에서 지움) · 기간이 지남(올리다 멈춘 지 24시간 · 임시 저장 30일 등)
|
||||
name: string | null // 기록이 남아 있으면(지운 뒤 30일) 이름 · 크기
|
||||
size: number | null
|
||||
}
|
||||
|
||||
/** 링크 안 파일 하나의 상태 — 받는 쪽에서 본 것 */
|
||||
type SharedFileStatus =
|
||||
| 'available' // 받을 수 있다 (검사에서 깨끗함)
|
||||
| 'unscannable' // 받을 수 있다 — "검사하지 못함" (암호 압축 · 검사 한도 밖 · 검사기를 쓰지 않는 개발)
|
||||
| 'scanning' // 검사 중 — 끝나면 받는다 (그동안 받기는 409 FILE_008)
|
||||
| 'infected' // 위험 — 막았다
|
||||
| 'limit-reached' // 이 파일의 내려받기 상한에 닿았다
|
||||
| 'revoked' // 보낸 사람(또는 운영)이 링크를 끊었다
|
||||
| 'expired' // 기간이 지났다
|
||||
| 'missing' // 파일을 찾을 수 없다 (저장소 복원 등 — 드묾)
|
||||
|
||||
/** 메일 속 대용량 첨부 하나 — MessageDetail.largeFiles */
|
||||
interface MessageLargeFile {
|
||||
linkId: string
|
||||
fileId: string
|
||||
name: string // 서버 기록의 이름 — 메일 글이 아니다
|
||||
size: number
|
||||
status: SharedFileStatus
|
||||
expiresAt: string // 링크가 끝나는 때 (아래 "기간 · 보관 · 정리")
|
||||
revokedAt: string | null // 링크를 끊은 때 (보낸 사람 · 운영) — 끊지 않았으면 null (2026-10-30 더함 — 화면의 "…에 끊었습니다")
|
||||
url: string // 공개 화면 주소 `<SHARE_PUBLIC_URL>/s/<token>` — 본문 덩이의 그 주소. 같은 링크의 파일은 같은 주소
|
||||
downloads: { // 보는 사람이 그 링크를 만든 사람일 때만 — 아니면 null
|
||||
count: number // 받은 수 (받기 시작한 수)
|
||||
lastDownloadedAt: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
/** GET /api/file-quota */
|
||||
interface FileQuota {
|
||||
usedBytes: number // 지금 잡고 있는 양 — 위 "한도"의 보관량
|
||||
limitBytes: number // 직원당 보관량 한도
|
||||
maxFileBytes: number // 한 파일
|
||||
maxFilesPerMessage: number // 메일 한 통에 새로 싣는 대용량 파일 수
|
||||
chunkSize: number // 16,777,216 — 올리기 응답의 chunkSize 와 같다
|
||||
retentionDays: number // 보낸 날부터 받을 수 있는 날 수 (30)
|
||||
}
|
||||
|
||||
/** GET /api/share-links 의 항목 — 내가 만든 링크 (보낸 메일 한 통) */
|
||||
interface ShareLink {
|
||||
id: string
|
||||
sentEmailId: string | null // 보낸 사본의 메일 id — [보낸 메일 보기]. 보낸 결과를 모르면 null (아래 "링크를 만드는 때")
|
||||
subject: string | null // 보낸 메일 제목 — 엔진에서 읽는다 (지웠거나 읽지 못하면 null)
|
||||
sentAt: string // 보낸 때 (예약은 보낼 시각)
|
||||
expiresAt: string
|
||||
status: 'active' | 'expired' | 'revoked' // revoked = 보낸 사람(또는 운영)이 끊었다
|
||||
revokedAt: string | null // 끊은 때 — revoked 일 때만 값이 있다 (2026-10-30 더함 — 설정의 "10월 22일에 끊음")
|
||||
files: SharedFile[] // 링크의 파일 (보낸 차례)
|
||||
}
|
||||
interface SharedFile {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
status: SharedFileStatus
|
||||
downloadCount: number
|
||||
lastDownloadedAt: string | null
|
||||
}
|
||||
|
||||
/** POST /api/share-link-lookups — 인용 속 링크의 상태 (누구의 링크든 상태 · 끝나는 날만) */
|
||||
interface ShareLinkLookupRequest {
|
||||
urls: string[] // 1~20개, 겹치지 않게 — 본문(인용)에서 찾은 공개 화면 주소, 한 개 300자까지
|
||||
}
|
||||
interface ShareLinkState {
|
||||
url: string // 보낸 그대로
|
||||
status: 'active' | 'expired' | 'revoked' | 'unknown' // unknown = 우리 링크 모양이 아님 · 모르는 · 보내기를 취소한 · 기록을 지운 링크
|
||||
expiresAt: string | null // unknown 이면 null
|
||||
}
|
||||
|
||||
/** GET /api/public-links/{token} — 로그인 없음 */
|
||||
interface PublicLink {
|
||||
status: 'active' | 'expired' | 'revoked'
|
||||
sender: { name: string | null; email: string } // 보낼 때 로그인한 계정의 이름 · 주소 — 보낸 사람이 꾸밀 수 없다
|
||||
sentAt: string
|
||||
expiresAt: string
|
||||
files: PublicLinkFile[] // active 일 때만 — 끝난 링크는 빈 배열 (파일 이름도 보이지 않는다)
|
||||
reportAddress: string // 신고 · 문의 주소 (SHARE_REPORT_ADDRESS)
|
||||
}
|
||||
interface PublicLinkFile {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
status: Exclude<SharedFileStatus, 'revoked' | 'expired'>
|
||||
}
|
||||
```
|
||||
|
||||
### 엔드포인트
|
||||
|
||||
| 메서드 · 경로 | 요청 | 성공 | 주요 실패 |
|
||||
| --- | --- | --- | --- |
|
||||
| `POST /api/file-uploads` | `FileUploadCreate` (아래 "올리기") | 201 `LargeFile` | 400 `VAL_001` · `FILE_001`, 409 `FILE_005`, 413 `FILE_002`, 429 `REQ_001`, 507 `FILE_006` |
|
||||
| `GET /api/file-uploads` | `cursor` · `limit`(기본 20, 최대 100) | 200 `{ items: LargeFile[], pageInfo }` — **보내지 않은 내 파일**(살아 있는 링크에 실리지 않은 것 — 올리는 중 포함), 만든 때 최근부터, `total` 포함 | 400 `VAL_001` |
|
||||
| `GET /api/file-uploads/{id}` | — | 200 `LargeFile` | 404 `RES_001` |
|
||||
| `PUT /api/file-uploads/{id}/chunks/{index}` | 조각 바이트 (`application/octet-stream`) | 200 `LargeFile` | 400 `VAL_001` · `FILE_004`, 404 `RES_001`, 409 `FILE_003`, 413 `FILE_002`, 429 `REQ_001`, 507 `FILE_006` |
|
||||
| `POST /api/file-uploads/{id}/completion` | — | 200 `LargeFile` (`ready`) | 404 `RES_001`, 409 `FILE_003`(빠진 조각 · 진행 중) |
|
||||
| `DELETE /api/file-uploads/{id}` | — | 204 — 올리기를 그만두거나 보내지 않은 파일을 지운다 | 404 `RES_001`, 409 `FILE_003`(살아 있는 링크에 실린 파일) |
|
||||
| `GET /api/file-quota` | — | 200 `FileQuota` | — |
|
||||
| `GET /api/share-links` | `cursor` · `limit`(기본 20, 최대 100) | 200 `{ items: ShareLink[], pageInfo }` — 보낸 때 최근부터, `total` 포함 | 400 `VAL_001` |
|
||||
| `POST /api/share-links/{id}/revocation` | — | 200 `ShareLink` — 그 링크(메일)를 끊는다 | 404 `RES_001` |
|
||||
| `POST /api/share-link-lookups` | `ShareLinkLookupRequest` | 200 `{ links: ShareLinkState[] }` — 요청 차례 그대로 | 400 `VAL_001` |
|
||||
| `GET /api/public-links/{token}` | — (로그인 없음) | 200 `PublicLink` | 404 `RES_001`, 429 `REQ_001` |
|
||||
| `GET`·`HEAD /api/public-links/{token}/files/{fileId}` | `Range`(선택) — 로그인 없음 | 파일 200 · 206 · 416 (표준 래퍼 없음) | 404 `RES_001`, 409 `FILE_008`, 410 `FILE_007`, 429 `REQ_001` |
|
||||
| `GET`·`HEAD /api/messages/{id}/large-files/{fileId}` | `Range`(선택) | 파일 200 · 206 · 416 (표준 래퍼 없음) | 404 `RES_001`, 409 `FILE_008`, 410 `FILE_007`, 503 `MAIL_001` |
|
||||
|
||||
- `/api/public-links` 밖은 모두 로그인 필요(쿠키 — 없으면 401 `AUTH_001`)이고, 상태를 바꾸는 요청은 출처 검사를 받는다(머리말).
|
||||
- 경로의 id 는 UUID 모양이 아니면 404 `RES_001`. 남의 것 · 지운 것 · 없는 것을 가리지 않고 404 다.
|
||||
- 공개 경로 밖의 응답은 `Cache-Control: private, no-store`.
|
||||
|
||||
### 올리기 (`/api/file-uploads`)
|
||||
|
||||
```ts
|
||||
interface FileUploadCreate {
|
||||
name: string // 1~255자 (JS 문자열 길이) — 서버가 정리한 이름(응답의 name)으로 둔다
|
||||
size: number // 바이트 — 1 이상 · 한 파일 한도 이하의 정수
|
||||
purpose: 'mail' // 쓰임 — 11단계는 'mail' 만 (2부 파일 저장소가 더한다)
|
||||
lastModified?: number | null // 선택 — 브라우저 File.lastModified (ms, 0 이상 정수)
|
||||
}
|
||||
```
|
||||
|
||||
1. **만들기** `POST /api/file-uploads` — 형식은 정리한 이름의 확장자로만 본다(밖이면 400 `FILE_001`). 브라우저가 주는 형식(MIME)은 받지 않는다 — 이 경로에서는 믿을 근거가 없고,
|
||||
내려줄 때 늘 `octet-stream` + `attachment` + `nosniff` + sandbox 이며 내용은 악성 검사가 본다(보안 규칙 "확장자와 MIME 둘 다"의 예외 — 열린 질문).
|
||||
그다음 크기(413 `FILE_002`) → 직원 한도(409 `FILE_005`) → 서버 총량 · 디스크 여유(507 `FILE_006`) 차례로 본다. 201 `LargeFile`(`uploading`, `receivedChunks: []`).
|
||||
2. **조각** `PUT /api/file-uploads/{id}/chunks/{index}` — `index` 는 0 ~ `chunkCount − 1`, 본문은 그 조각의 바이트 그대로.
|
||||
- `Content-Type: application/octet-stream` 만 받는다(JSON 이면 본문 파서의 1MB 한도에 먼저 걸려 413, 그 밖은 400 `VAL_001`).
|
||||
조각은 JSON 본문 파서 · 모양 한도를 지나지 않고(두 파서 모두 `application/json` 만 읽는다 — `app.setup.ts:164-170, 208-220`) 처리기가 요청 스트림을 직접 읽는다.
|
||||
- `Content-Length` 필수 = `chunkSize`(마지막 조각만 `size − chunkSize × (chunkCount − 1)`). 없거나(청크 전송) 다르면 읽기 전에 400 `VAL_001`. 압축(`Content-Encoding`)도 400 `VAL_001`.
|
||||
- 선택 `Content-Digest: sha-256=:<base64>:`(RFC 9530) — 웹은 늘 보낸다. 받은 바이트와 다르면 400 `FILE_004`(그 조각을 받지 않았다 — 같은 조각을 다시 보낸다). sha-256 이 없으면(다른 알고리즘만) 없는 것으로 본다.
|
||||
sha-256 값의 모양이 틀리면(base64 로 32바이트가 아니면) 읽기 전에 400 `VAL_001`(구현 — 2026-10-30).
|
||||
- 조각 번호는 앞자리 0 없는 10진수(0 ~ 999,999)다 — `01` · `+1` · ` 1` · `1.0` · `1e1` · `0x1` · `0b1` 등은 400 `VAL_001`(숫자로 바꿔 보지 않는다 — 2026-10-30 리뷰). 본문을 다 받지 못했으면(길이가 다름 · 클라이언트가 끊음) 400 `VAL_001`, 디스크 등 그 밖의 오류는 500 `SYS_001`(error 로그).
|
||||
- 다 받아 디스크에 쓰고(`datasync`) 나서야 받은 것으로 적는다 — 중간에 끊기면 적지 않는다. 서버는 조각마다 SHA-256 을 계산해 둔다(맨 앞 조각의 값이 `firstChunkSha256`).
|
||||
- **다시 보내기**: 받지 않은 조각은 쓴다. 이미 받은 조각은 쓰지 않고 내용을 견준다 — 같으면 200 그대로, 다르면 409 `FILE_003`(아무것도 바꾸지 않는다).
|
||||
같은 조각을 동시에 보내면 뒤의 것, 이미 끝낸 올리기에 보내면 409 `FILE_003`.
|
||||
- 차례는 자유다(빠진 조각만 다시). 웹은 파일마다 한 번에 한 조각.
|
||||
- 디스크가 차면(`ENOSPC`) 507 `FILE_006`(error 로그) — 그 조각은 받지 않았다.
|
||||
- 200 `LargeFile`(받은 조각 반영).
|
||||
3. **끝내기** `POST /api/file-uploads/{id}/completion`(본문 없음) — 조각이 모두 있으면 파일로 만들고(`ready`) 곧바로 악성 검사 차례에 넣는다 → 200 `LargeFile`(`scanStatus: 'pending'` — 검사기를 쓰지 않으면 `'unscannable'`).
|
||||
빠진 조각이 있으면 409 `FILE_003`(클라이언트는 `GET` 으로 빠진 번호를 보고 보낸다), 끝내기가 진행 중이어도 409 `FILE_003`. 이미 끝냈으면 그대로 200(응답을 못 받은 재시도).
|
||||
파일 전체의 해시는 따로 보지 않는다(조각마다 봤다).
|
||||
4. **상태** `GET /api/file-uploads/{id}` — 끊긴 뒤 무엇을 받았는지 본다. 올리는 중은 마지막 조각 뒤 24시간 지나면 404 다(아래 "기간 · 보관 · 정리").
|
||||
5. **보내지 않은 파일 목록** `GET /api/file-uploads` — 살아 있는 링크(보낸 메일)에 실리지 않은 내 파일(올리는 중 · 다 올림)이다. 보내기를 취소해 되돌린 파일도 여기 든다. 설정의 "보내지 않은 파일"과 [지우기]가 쓴다.
|
||||
6. **그만두기 · 지우기** `DELETE /api/file-uploads/{id}` — 올리는 중이면 그만두고, 다 올렸어도 살아 있는 링크에 실리지 않았으면 지운다 — 보관량에서 바로 빠진다.
|
||||
그 파일을 싣고 있던 임시 저장본을 열면 `MissingLargeFile`(`reason: 'deleted'` — "지운 파일")로 온다. 살아 있는 링크에 실린 파일은 409 `FILE_003`(링크 끊기로 — 아래 "보낸 사람 관리").
|
||||
|
||||
**요청 수 · 동시 수**
|
||||
|
||||
| 대상 | 한도 | 넘으면 |
|
||||
| --- | --- | --- |
|
||||
| 조각 `PUT` | IP 마다 1분 1,200회 (이 경로만 `@Throttle`) · 직원마다 동시 4개 · 서버 전체 동시 32개 | 429 `REQ_001` |
|
||||
| 만들기 · 끝내기 · 상태 · 목록 · 지우기 | 전역 한도 (경로마다 IP 당 — `THROTTLE_*`, 기본 1분 100회) | 429 `REQ_001` |
|
||||
|
||||
- 조각 경로만 크게 둔다 — 16MiB 조각은 한 사람이 1분에 수십 번 보내고 사무실은 IP 하나로 나간다(전역 한도에 걸리면 큰 파일이 429 로 멈춘다).
|
||||
두 번째 limiter 가 아니라 라우트의 `@Throttle` 이다(외부 이미지 중계와 같은 결 — `mail-image.controller.ts:79`).
|
||||
- 동시 수는 기다리지 않는다 — 넘으면 429, 클라이언트가 잠시 뒤 같은 조각을 다시 보낸다. 잠금 · 셈은 서버 메모리다(한 인스턴스 전제).
|
||||
- 앞단 nginx 는 이 경로만 본문 17MB · 읽기 90초이고, 요청 본문을 다 받아 둔 뒤 백엔드에 넘긴다(느린 연결은 nginx 가 받는다 — 아래 "운영 확인").
|
||||
|
||||
**클라이언트**
|
||||
- 조각 요청은 `silent` 로 보내고 실패는 그 파일 줄에 보인다(첨부의 `FILE_001` · `FILE_002` 처럼).
|
||||
네트워크 오류 · 시간 초과 · 429 · 502 · 503 · 504 는 같은 조각을 저절로 다시 보내고(간격을 늘리며 — 구현하며 정함), 넘으면 멈추고 [다시 시도](받은 조각은 남는다). 401 은 지금 규칙(갱신 한 번 → 같은 조각을 다시).
|
||||
`FILE_004` 는 그 조각을 다시, `FILE_003` 은 `GET` 으로 상태를 보고 잇는다. `FILE_001` · `FILE_002` · `FILE_005` · `FILE_006` · 404 는 멈추고 알린다.
|
||||
구현하며 정함(2026-10-30 — 웹):
|
||||
- 만들기는 파일을 고르자마자 모두 보낸다(임시 저장본에 바로 실린다). 조각은 파일 3개까지만 함께 보내고 나머지는 차례를 기다린다(위 직원당 동시 4개는 조각 `PUT` 만 센다).
|
||||
- 저절로 다시 보내는 간격은 2 · 5 · 15 · 30초, 1분 넘게 잇지 못하면 멈추고 [다시 시도]. 요청 시간 한도는 조각 5분 · 그 밖 30초.
|
||||
- 조각이 `FILE_003` 인데 상태가 "받음"이면 그 조각을 한 번 더 보내 200 을 확인한다(같은 내용이면 200) — 또 `FILE_003` 이면 멈춘다(내용이 다른 조각).
|
||||
- 끝내기가 진행 중(`FILE_003`)이면 2 · 5 · 15 · 30초 간격으로 다시 묻고 1분 넘게 그대로면 멈추고 [다시 시도](끝내기는 거듭해도 같다).
|
||||
- 404(서버가 올리기를 잊음)는 멈추고 [다시 시도]를 누를 때만 새로 만들어 처음부터 올린다 — 저절로 다시 올리지 않는다.
|
||||
- 검사 상태 다시 묻기: 쓰기 화면 · 읽기 화면은 15 · 30 · 60초 간격 최대 30번, 공개 화면은 20번(공개 경로의 IP 한도 안).
|
||||
- 진행은 받은 바이트로 보인다("420MB / 1.2GB · 약 3분"), 멈춤 · 빼기. 빼면 `DELETE`(알림 없이 — 404 는 지운 것으로 본다). 쓰기 화면을 버리면(휴지통) 지우지 않는다 — 되돌리면 다시 쓴다.
|
||||
- **새로고침 · 창 닫힘 뒤 이어 올리기(처음 판에 넣는다)**: 올리는 중인 파일도 임시 저장본 머리글에 실린다(아래 "임시 저장") — 다시 열면 `uploading` 으로 오고
|
||||
"올리다 멈춘 파일 — 같은 파일을 다시 고르면 이어서 올립니다"(브라우저는 고른 파일을 스스로 다시 읽지 못한다).
|
||||
다시 고른 파일은 **이름 · 크기 · `lastModified` 가 같고 그 파일의 맨 앞 조각 SHA-256 이 `firstChunkSha256` 과 같을 때** 같은 파일로 보고 빠진 조각부터 잇는다.
|
||||
다르면 새로 만든다(앞 올리기는 `DELETE`). 맨 앞 조각을 받기 전(`firstChunkSha256: null`)이면 이을 것이 없다 — 새로 만든다.
|
||||
올리는 중은 마지막 조각 뒤 24시간 남는다 — 그 뒤에 열면 `MissingLargeFile`(`reason: 'expired'`)이라 다시 올린다.
|
||||
- 올리는 동안은 보내기를 막는다(아래 "보내기를 막는 까닭"). 나가기 확인에 올리는 중인 파일을 넣는다(2절 "임시 저장"의 나가기 규칙).
|
||||
|
||||
### 쓰기 화면이 고르는 것 (클라이언트)
|
||||
|
||||
- **링크로 바뀌는 때**: 파일 하나가 `attachmentMaxBytes`(20MB — `GET /api/mail-quota`)를 넘거나, 일반 첨부 합계가 `attachmentTotalMaxBytes`(25MB)를 넘게 만드는 파일부터 자동으로 대용량(링크)이다.
|
||||
일반 첨부와 한 메일에 섞는다. 서버는 바꾸지 않는다 — 보낸 대로 다룬다.
|
||||
- **손으로 「링크로 보내기」**는 이 쓰기 화면에서 고른 파일(브라우저에 내용이 있는 것)에만 있다. 전달하는 원본 메일의 첨부 · 임시 저장본의 첨부(엔진에 있는 것)는 처음 판에서 바꾸지 않는다. 링크 → 일반 첨부로 되돌리기도 처음 판에는 없다.
|
||||
- **올리기 전에 막는다**(지금 첨부 사전 검사와 같은 결): 형식(대용량 허용 목록 — 짝 상수), 크기(`maxFileBytes`), 한 통의 개수(`maxFilesPerMessage`), 보관량(`usedBytes` + 이 파일 > `limitBytes`).
|
||||
값은 `GET /api/file-quota` 로 받는다(못 받으면 위 기본값).
|
||||
- 받는 사람이 사내뿐이어도 같은 규칙이다.
|
||||
|
||||
**보내기를 막는 까닭** — 일반 첨부의 까닭(올리는 중 · 찾지 못함)에 더한다. 단추는 `aria-disabled` + 까닭 글로 막는다(글 · 모양은 디자인 README). 서버도 같은 것을 막는다.
|
||||
|
||||
| 까닭 코드 (클라이언트) | 상태 | 서버가 받으면 |
|
||||
| --- | --- | --- |
|
||||
| `large-file-uploading` — 올리는 중 · 멈춘 올리기 · 다시 골라야 이어지는 파일 | `LargeFile.status: 'uploading'` | 400 `MAIL_016` |
|
||||
| `large-file-missing` — 지운 파일 · 기간이 지난 파일 | `MissingLargeFile`(`reason`) | 400 `MAIL_016` |
|
||||
| `large-file-infected` — 위험 판정 | `scanStatus: 'infected'` | 400 `MAIL_016` |
|
||||
| `large-file-count` — 한 통의 개수 초과 | `largeFileIds` 가 `maxFilesPerMessage` 보다 많음 | 400 `VAL_001` |
|
||||
|
||||
- 검사 중(`pending`) · 검사하지 못함(`unscannable`)은 막지 않는다 — 받는 쪽이 "검사 중"을 보고 끝나면 받는다. 보관량은 보내기가 아니라 올리기 전에 막는다(만들기의 409 `FILE_005`).
|
||||
|
||||
### 발송 · 임시 저장과 함께
|
||||
|
||||
**본문 필드** (발송 · 임시 저장 같다 — 2절 "발송 본문"에 더한다)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// … 2절 그대로 …
|
||||
"largeFileIds": ["<올리기 id>"], // 선택 — 이 메일에 새로 싣는 내 대용량 파일, 겹치지 않게, 20개까지 (남용 방지 — 실제 한도는 maxFilesPerMessage)
|
||||
"bodyQuoteStart": 1234 // 선택 — body(텍스트)에서 인용이 시작하는 자리 = 덩이를 넣을 자리
|
||||
}
|
||||
```
|
||||
|
||||
- **인용 경계 — 서식 본문**: 쓰기 화면은 인용("원본 메일" 머리) 바로 앞에 빈 표시 `<div data-ara-quote-start></div>` 하나를 둔다.
|
||||
보내는 정화는 이 속성을 `div` 에만 남기고(그 밖의 `data-*` 는 지금처럼 지운다 — `sanitize-outgoing-html.ts:75-82`), 서버는 첫 표시 자리를 덩이 자리로 쓴 뒤 표시를 **모두 지운다** —
|
||||
받는 사람에게 가지 않고, 임시 저장본에도 남지 않고, 지문에도 들지 않는다. 지금 쓰기 화면이 보내기 전에 지우는 인용 머리 표시(`data-quote-header` — `mail-serialize.ts:202-206`)와는 따로다.
|
||||
- **인용 경계 — 텍스트**: `bodyQuoteStart` — 0 ~ `body.length`(JS 문자열 위치)이고 줄의 처음이어야 한다(0 이거나 바로 앞이 줄바꿈). 아니면 400 `VAL_001`.
|
||||
서버는 텍스트 본문을 고치지 않고 넘기므로(`mail.service.ts:1192`) 클라이언트가 센 자리 그대로다. 지문에 넣지 않는다.
|
||||
- 표시 · `bodyQuoteStart` 가 없으면 덩이는 맨 끝(서명 아래)이다. 대용량 파일이 없으면 둘 다 쓰지 않는다(표시는 지운다).
|
||||
|
||||
**보내기 전 확인** (`POST /api/messages` — 엔진에 묻기 전이라 메일은 나가지 않는다)
|
||||
- `largeFileIds` 의 파일은 있고 · 내 것이고 · 다 올렸고(`ready`) · 위험이 아니어야 한다. 아니면(없음 · 남의 것 · 지움 · 기간 지남 · 올리는 중 · 위험) 400 `MAIL_016` — 다 받았는지 서버가 확인한 뒤에만 보낸다.
|
||||
검사 중(`pending`) · 검사하지 못함은 보낸다(보내는 사람을 기다리게 하지 않는다).
|
||||
- 개수가 `maxFilesPerMessage` 를 넘으면 400 `VAL_001`.
|
||||
- 클라이언트는 `MAIL_016` 이면 파일마다 `GET /api/file-uploads/{id}` 로 다시 물어(404 면 없는 파일) 쓸 수 없는 것을 보이고 빼게 한다 — `MAIL_004` 와 같은 흐름. 쓰던 메일 · 키는 그대로다.
|
||||
|
||||
**덩이 — 서버가 본문에 넣는 안내**
|
||||
- 보낼 때만 만든다(임시 저장본에는 넣지 않는다). 한 통에 하나 — 이번 발송의 새 링크(내 파일) 묶음이다.
|
||||
- **자리**: 서식 본문은 첫 경계 표시 자리, 텍스트는 `bodyQuoteStart` — 없으면 맨 끝. 그래서 **내 글 · 서명 아래, 인용 위**다.
|
||||
- **내용** — HTML 과 텍스트에 같은 내용(글 · 모양은 디자인 README "11단계", 구현하며 맞춘다):
|
||||
개수 · 합계 · 끝나는 날("2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간)"), 파일마다 이름 · 크기, 내려받기 주소(**주소 글자가 보이는 링크** — 단축 주소 없음),
|
||||
"내려받을 때 로그인이나 비밀번호를 묻지 않습니다", 영어 한 줄("Large files — download by Nov 29, 2026 (KST)"). 받는 사람 · 제목은 넣지 않는다.
|
||||
크기는 화면과 같은 표기(1024 단위 — `1.2 GB` · `612 MB`, 짝: frontend `shared/utils/format.ts` 의 `formatFileSize`), 날짜는 한국 날짜.
|
||||
- **HTML 모양**: 보내는 정화의 허용 목록 안의 태그 · style 값으로만 만든다(2절 "서식 본문" — div · p · span · a · br · table, 색 hex · 여백 px · 글자 8~99px …) —
|
||||
다시 읽을 때 · 답장 · 전달의 인용 · 보내기 취소로 되돌릴 때도 모양이 남게. 그림 · 뱃지 · 알약 없이 테두리 상자와 글자.
|
||||
- **표시**: HTML 덩이의 앞뒤에 주석 `<!--ara-large-files:v1-->` · `<!--/ara-large-files-->` 를 둔다. 보내는 정화는 HTML 주석을 지우므로(sanitize-html 2.18 — `<div>a<!--x-->b</div>` → `<div>ab</div>`, 2026-10-30 확인)
|
||||
사용자가 쓴 본문(인용 포함)에는 이 표시가 들어갈 수 없다 — 서버만 넣는다. 받는 쪽 메일 프로그램에는 보이지 않는다.
|
||||
- **텍스트 덩이**: 같은 내용을 줄로 — 앞뒤에 빈 줄.
|
||||
- **한도**: 넣은 뒤 다시 잰다 — 텍스트 100,000자 · 서식 본문 UTF-8 2,000,000바이트. 넘으면 400 `MAIL_007`(메일도 링크도 만들지 않는다).
|
||||
덩이는 파일 10개 · 이름 255자여도 HTML 수십 KB 안이다 — 클라이언트는 지금처럼 보내기 직전에 재고 넘으면 인용을 줄인다(2절 "답장·전달 초안의 길이"). 인용 위라 다시 읽기 한도에서 잘리는 일도 드물다.
|
||||
|
||||
**지문 · 같은 키 재시도**
|
||||
- 덩이는 **정화 · 서명 그림 · 지문 · 중복 확인을 마친 뒤**, 엔진에 보내기 바로 앞에 넣는다(`mail.service.ts:1036-1117` 의 `deliverOnce` — 지문 `:1056` · 이미 보냈는지 `:1058` · 보내기 `:1099`).
|
||||
앞에 넣으면 시도마다 새 토큰이 지문에 들어가 같은 키의 재시도가 `MAIL_005` 가 된다.
|
||||
- 지문에는 덩이 대신 `largeFileIds`(정렬)를 **있을 때만** 넣는다 — 대용량 파일이 없는 발송의 지문은 지금과 같다(`send-idempotency.ts:109-128` — 서식 본문을 있을 때만 붙이는 것과 같은 결).
|
||||
경계 표시 · `bodyQuoteStart` 는 넣지 않는다.
|
||||
- 같은 키의 재시도가 처음 메일을 찾으면 처음 결과를 그대로 준다 — 링크를 새로 만들지 않는다. 대용량 파일을 바꾼 재시도는 409 `MAIL_005`.
|
||||
|
||||
**링크를 만드는 때 · 발송이 실패하면**
|
||||
- 이번 발송에 내 파일이 있으면 링크 하나를 만든다 — 중복 확인 뒤 · 엔진에 보내기 바로 앞. 토큰(아래 "공개 내려받기"), 보낸 사람 이름 · 주소(로그인한 계정의 그때 값), 보낸 때, 끝나는 때를 둔다.
|
||||
보내기에 성공하면 보낸 사본의 메일 id 를 잇는다. DB 쓰기 한 번이라 발송 55초 예산에 영향이 거의 없다.
|
||||
- **보내지 않은 것이 확실한 실패**(받는 사람 모두 거절 `MAIL_010` · `MAIL_011`, 엔진이 요청을 거절한 `VAL_001` · `MAIL_004` · `FILE_002` · `MAIL_003` · `AUTH_003` · `REQ_001`(구현 때 더함 — 엔진의 요청 수 한도 거절도 제출 전이다),
|
||||
시간이 모자라 시작하지 않은 `MAIL_001` — 어댑터가 "시작하지 않음"으로 표시한 것만(`SendNotStartedException` — 로그 전용인 context 의 글로 가르지 않는다))면
|
||||
그 링크를 **취소한 링크**로 둔다 — 공개 경로는 404(없는 링크)이고 보낸 사람 목록에 나오지 않는다.
|
||||
- **결과를 모르는 실패**(제출 중 시간 초과 · 연결 끊김 — `MAIL_001`)는 그대로 둔다(나갔을 수 있다). 같은 키의 재시도가 그 메일을 찾으면 그 링크에 보낸 사본을 잇는다.
|
||||
찾지 못해 새로 보내면 새 링크가 생기고, 앞 링크는 보낸 메일을 모르는 채(`sentEmailId: null`) 기간까지 산다 — 목록에서 끊을 수 있다(드묾 — 2절 중복 방지의 알려진 한계와 같은 경우).
|
||||
- 일부 받는 사람만 거절(201 `rejectedRecipients`)은 보낸 것이다 — 링크는 산다.
|
||||
|
||||
**임시 저장**
|
||||
- 저장 본문의 `largeFileIds` 는 내 파일이면 **올리는 중이어도** 된다(새로고침 뒤 이어 올리기). 없음 · 남의 것 · 지움 · 기간 지남은 400 `MAIL_016` —
|
||||
첨부의 `MAIL_004` 처럼 클라이언트는 그런 파일(`MissingLargeFile`)이 남은 동안 저장하지 않는다(사용자가 빼면 다시 저장한다). 위험 판정 파일도 저장은 된다(보낼 때 `MAIL_016`).
|
||||
- 서버는 초안에 머리글로 둔다 — `X-Ara-Large-Files: <파일 id>, …`(올리는 중인 올리기 id 도 — 같은 id 다. 답장 대상 머리글 `X-Ara-Reply-To-Id` 와 같은 길 — `jmap-mail.provider.ts:193, 2946`). 덩이는 넣지 않는다.
|
||||
파일 id 는 저장해도 바뀌지 않는다 — 일반 첨부처럼 응답으로 바꿔 쓸 일이 없다(`DraftSaved` 는 그대로다).
|
||||
- 저장하면 그 파일들을 "임시 저장에 실린 파일"로 적는다 — 다 올린 파일의 남는 기간이 24시간에서 다 올린 날부터 30일로 는다(아래 "기간 · 보관 · 정리"). 올리는 중인 파일은 그대로 마지막 조각 뒤 24시간이다.
|
||||
- `GET /api/drafts/{id}`: 머리글의 파일 가운데 **그 직원의 것만** `largeFiles` 로 준다 — 있으면 `LargeFile`, 지웠거나 기간이 지났으면 `MissingLargeFile`(`reason`).
|
||||
남의 id 와 기록까지 지워진 id 는 버린다(다른 프로그램이 만든 초안일 수 있다 — 둘은 가릴 수 없다).
|
||||
- 알려진 한계(답장 대상 머리글과 같다): 다른 메일 프로그램이 그 초안을 그대로 보내면 머리글만 나가고 덩이는 없다.
|
||||
|
||||
**보내기 취소 · 예약 발송** (2절, 9단계)
|
||||
- 덩이는 제출할 때 메일 안에 들어 있다 — 엔진이 붙잡았다가 그대로 보낸다.
|
||||
- 링크의 보낸 때: 예약은 `sendAt`(끝나는 날도 그 날짜로 센다 — 예약 7일 + 30일이면 37일까지 보관), 보내기 취소를 기다리는 발송은 요청한 때(몇 초 차이는 무시).
|
||||
예약 링크는 보낼 시각 전에도 열린다(주소를 아는 사람은 보낸 사람뿐이다).
|
||||
- 취소(`DELETE /api/scheduled-messages/{id}`)가 엔진에서 확인된 뒤 그 발송이 만든 링크를 취소한 링크로 둔다. 파일은 남는다(되돌린 초안이 쓴다 — 임시 저장 기간으로, 보내지 않은 파일 목록에 다시 나온다).
|
||||
- 되돌린 초안(취소 표시 `$ara-canceled`)을 열면 서버가 원문 HTML 의 표시 사이(덩이)를 정화 **전에** 떼고(`mail.service.ts:1282-1304` 의 `draftHtml` — 취소한 발송의 서명 그림을 되돌리는 그 자리),
|
||||
텍스트 본문의 덩이도 뗀다(서버가 넣은 글 그대로일 때 — 손대면 남는다). 그 링크의 파일을 `largeFiles` 로 돌려준다 → 쓰기 화면은 대용량 목록으로 보이고, 다시 보내면 새 링크다.
|
||||
덩이를 떼지 못하면 다시 보낼 때 덩이가 둘이 된다 — 테스트로 지킨다. 인용 속 남의 덩이(표시 없는 글)는 건드리지 않는다.
|
||||
- 붙잡힌 메일은 우리 API 로 옮기거나 지울 수 없다(`MAIL_015`) — 그동안 링크 · 파일도 그대로다.
|
||||
- ~~⚠️ 구현 전 확인: 엔진이 보낸 사본의 HTML 주석을 그대로 두는지~~ — 확인함(2026-10-30, 개발 엔진 Stalwart 0.16.27): 만든 메일을 다시 읽으면 HTML 주석 · 텍스트 본문(줄바꿈 그대로)과
|
||||
`X-Ara-Large-Files` 머리글(`header:X-Ara-Large-Files:asText`)이 그대로 돌아온다. e2e 가 취소 → 되돌린 초안 → 다시 보내기에서 덩이가 하나인지 본다.
|
||||
|
||||
**답장 · 전달**
|
||||
- 원본의 대용량 첨부를 다시 싣지 않는다 — 원래 메일의 덩이는 **인용 속 글(링크)로 그대로** 간다(편집기가 `data-ara-large-files` 를 버린다 — 링크는 보통 링크다).
|
||||
서버는 인용 속 링크를 다시 싣거나 우리 링크로 바꾸지 않는다. 받는 사람은 그 링크로 원래 공개 화면에서 받는다 — 원래 파일의 받은 수 · 상한 · 끝나는 날을 따른다.
|
||||
- 전달 · 답장하는 사람이 새 대용량 파일을 더하면 그 메일의 링크는 하나(그 사람 것)이고, 새 덩이는 인용 위에 들어간다.
|
||||
- 쓰기 화면은 인용 속 원래 링크의 상태(끝나는 날 · 기간 지남 · 공유 멈춤)를 첨부 칸 아래 **글자로만** 알린다 — 아래 `POST /api/share-link-lookups`.
|
||||
|
||||
### 인용 속 링크의 상태 (`POST /api/share-link-lookups`)
|
||||
|
||||
- 쓰기 화면(전달 · 답장)이 인용에서 찾은 공개 화면 주소를 보내면 링크마다 **상태 · 끝나는 날만** 준다 — 누구의 링크든 같다(이름 · 보낸 사람 · 받은 수는 주지 않는다. 주소를 가진 사람은 공개 화면에서 더 많이 본다).
|
||||
- 클라이언트는 주소의 경로가 `/s/` + 43자(`[A-Za-z0-9_-]`)인 링크를 고른다. 서버는 `<SHARE_PUBLIC_URL>/s/<토큰>` 모양이 아니거나 모르는 · 보내기를 취소한 · 기록을 지운 링크면 `unknown` 이다.
|
||||
이 길에서는 주소 뒤의 쿼리 · `#` 은 보지 않는다(받는 쪽 보안 장비가 붙이기도 한다 — 구현). 읽기 화면의 덩이 알아보기는 쿼리 · `#` 이 없는 주소만 받는다.
|
||||
- 상태: `active`(받을 수 있는 기간 — `expiresAt`), `expired`, `revoked`. 파일마다의 상태(검사 중 · 위험 등)는 주지 않는다 — 받는 사람이 공개 화면에서 본다.
|
||||
- 주소(토큰)를 쿼리에 싣지 않으려고 `POST` 본문으로 받는다(요청 주소가 로그에 남는다 — 아래 "로그"). 상태를 바꾸지 않는다. 본문 검사(1~20개 · 겹침 · 300자)에 걸리면 400 `VAL_001`.
|
||||
- **클라이언트**: 첨부 칸 아래에 "인용한 대용량 첨부 링크 — 11월 29일까지 받을 수 있습니다" · "기간이 지났습니다" · "보낸 사람이 공유를 멈췄습니다"(글은 디자인 README). `unknown` 은 알리지 않는다. 알림 없이(`silent`) 부르고 실패하면 글자를 두지 않는다.
|
||||
|
||||
### 우리 웹메일에서 읽기 (`MessageDetail.largeFiles` · `GET /api/messages/{id}/large-files/{fileId}`)
|
||||
|
||||
- **첨부처럼 올리는 것은 셋이 모두 맞을 때만이다**:
|
||||
1. 원문 HTML(정화 전)의 표시 주석 사이에 있는 링크다 — 표시가 없으면 DB 를 보지 않는다.
|
||||
2. `href` 가 `<SHARE_PUBLIC_URL>/s/<토큰>` 꼴이고 토큰이 우리 기록에 있다(보내기를 취소한 링크는 빼고 — 한 통에 10개까지 본다).
|
||||
3. 그 링크를 만든 직원이 **그 메일의 보낸 사람**(머리글 From 의 주소 — 대소문자 무시)이다.
|
||||
- 맞으면 그 링크의 파일을 `largeFiles` 로 준다 — 이름 · 크기 · 상태는 메일 글이 아니라 서버 기록이다. `htmlBody` 에서는 그 덩이의 바깥 요소에 `data-ara-large-files` 를 붙인다.
|
||||
받은 HTML 에 있던 같은 속성은 정화가 지운다(받은 메일 정화의 속성 허용 목록에 `data-*` 가 없다 — `sanitize-mail-html.ts:165-`) — 그래서 서버만 붙일 수 있다.
|
||||
- **맞지 않으면 본문 글로 둔다** — 바깥이 흉내 낸 덩이, 바깥 사람 답장의 인용 속 링크, 전달된 원래 링크(인용 속 덩이에는 표시가 없고 보낸 사람도 다르다), 다른 주소. 속성도 `largeFiles` 도 없고 링크는 다른 링크처럼 새 창이다.
|
||||
- 텍스트만 있는 메일과, HTML 이 정화 한도를 넘어 비운 메일(`isHtmlBodyOmitted`)의 덩이는 알아보지 않는다(알려진 한계).
|
||||
- `downloads`(받은 수 · 마지막으로 받은 때)는 보는 사람이 그 링크를 만든 사람일 때만 붙는다 — 보낸편지함의 사본, 나에게도 보낸 메일.
|
||||
- 목록의 `hasAttachment`(클립)는 지금처럼 일반 첨부만 본다 — 대용량 첨부만 있는 메일은 클립이 없다(처음 판에서 뺀다).
|
||||
- **받기** `GET`·`HEAD /api/messages/{id}/large-files/{fileId}` — 그 메일에서 첨부처럼 올린 파일만 준다(아니면 404 `RES_001`).
|
||||
머리글 · 범위 · 상태별 실패는 아래 "공개 내려받기"와 같고(`FILE_007` · `FILE_008`), 엔진을 읽지 못하면 503 `MAIL_001`. 요청 수는 IP 마다 1분 300회.
|
||||
보는 사람이 링크를 만든 사람이면 **세지 않는다**(보낸 사람 자신의 확인). 다른 직원(사내 받는 사람)은 공개 내려받기처럼 센다.
|
||||
공개 경로에서는 보낸 사람을 알아볼 수 없다 — 포털 쿠키는 호스트 한정(Domain 없음 — `auth-cookies.ts:29-36`)이라 공개 호스트로 가지 않는다.
|
||||
- **클라이언트**: 첨부 칸에 첨부처럼 — 이름 · 크기 · "11월 29일까지". 받을 수 없으면 흐리게 + 글자("검사 중" · "기간 지남" · "공유 멈춤" · "위험" · "횟수 초과" · "찾을 수 없음"), "검사하지 못함"은 받을 수 있는 채 글자만.
|
||||
덩이(`data-ara-large-files` 요소)는 접어 둔다 — 그리기 전에 떨어진 문서에서 처리한다(클라이언트 정화가 `data-*` 를 지우므로 그 전에 — `frontend/src/shared/utils/mail-html.ts:85`).
|
||||
받기는 첨부처럼 `<a href>` 이동이고, 이동 전에 세션을 확인한다(`confirmSession()`).
|
||||
보낸 사람 자신에게는 "3회 받음 · 11월 29일까지" · [링크 복사](덩이의 주소 — `url`) · [링크 끊기](그 메일의 링크 — 확인 대화상자, 처음 초점 「취소」).
|
||||
|
||||
### 보낸 사람 관리 (`/api/share-links` · `/api/file-uploads` · `/api/file-quota`)
|
||||
|
||||
- **보낸 링크 목록** `GET /api/share-links` — 내가 만든 링크(취소한 링크는 빼고), 보낸 때 최근부터. 기록을 지우기 전(끝나거나 끊긴 뒤 90일)까지 기간 지남 · 끊음도 나온다.
|
||||
`subject` 는 그 쪽의 보낸 사본을 엔진에서 한 번에 읽는다 — 지웠거나 엔진을 읽지 못하면 null 이고 목록은 그대로 준다(warn).
|
||||
- **링크 끊기** `POST /api/share-links/{id}/revocation` — **그 링크(메일)의 파일 모두**의 공유를 멈춘다(파일 하나만 끊지 않는다). 되돌릴 수 없다. 200 `ShareLink`(`revoked`).
|
||||
다음 요청부터 공개 화면은 "공유 멈춤"이고 받기는 410 `FILE_007` 이다 — 이미 시작한 내려받기는 끝까지 간다.
|
||||
그 파일들이 다른 살아 있는 링크 · 임시 저장에 없으면 보관량에서 바로 빠지고 정리 때 지운다. 이미 끊었거나 기간이 지났으면 바꾸지 않고 200(지금 상태). 남의 링크 · 없는 것은 404 `RES_001`.
|
||||
- **링크 복사는 목록에 두지 않는다** — 토큰은 SHA-256 만 두어 되살릴 수 없다. 보낸 사람은 보낸 메일 본문의 덩이에서 복사한다(읽기 화면 — `largeFiles[].url`).
|
||||
- **보내지 않은 파일** `GET /api/file-uploads` · [지우기] `DELETE /api/file-uploads/{id}` — 보관량이 바로 준다. 그 파일을 싣고 있던 임시 저장본을 열면 "지운 파일"(`MissingLargeFile` · `reason: 'deleted'`)로 보인다.
|
||||
- **사용량** `GET /api/file-quota`.
|
||||
- **클라이언트**: 설정 › 메일 › 대용량 첨부 —
|
||||
- 맨 위 사용량("6.2 / 10 GB" — `formatQuotaUsage`).
|
||||
- 보낸 링크는 **메일(링크)마다 한 덩이**: 메일 제목 · 보낸 날 · 끝나는 날("n일 남음") · 상태(사용 중 · 만료 · 회수), 파일마다 이름 · 크기 · 받은 수 · 마지막으로 받은 때 · 파일 상태(검사 중 · 위험 · 검사하지 못함 · 횟수 초과),
|
||||
[보낸 메일 보기](`sentEmailId` — null 이면 없다) · [링크 끊기](확인 — "이 메일로 보낸 대용량 파일 N개를 더는 받을 수 없게 됩니다", 처음 초점 「취소」).
|
||||
- 보내지 않은 파일: 이름 · 크기 · 상태(올리는 중 · 다 올림 · 검사 상태) · 지워지는 때 · [지우기].
|
||||
|
||||
### 공개 내려받기 (로그인 없음)
|
||||
|
||||
- 누구나(바깥 거래처 · 내려받기 관리자 · 받는 쪽 보안 장비) 부른다. 로그인 · 쿠키를 쓰지 않는다(읽지도 않는다). 상태를 바꾸지 않는 `GET` 이라 출처 검사도 해당 없다.
|
||||
- **응답에 실리는 메일 정보는 보낸 사람 이름 · 주소뿐이다** — 제목 · 본문 · 받는 사람 · 메일 id 는 싣지 않는다(링크가 전달되면 그 사람도 본다 — 숨은참조가 드러나지 않게). 링크에 받는 사람 정보를 넣지도 않는다.
|
||||
|
||||
**토큰**
|
||||
- 링크마다 `crypto.randomBytes(32)` 의 base64url(43자, `=` 없음 — 256비트). 서버 서명 키(`JWT_SECRET`)에서 만들지 않는다(보안 규칙).
|
||||
- DB 에는 **SHA-256 만** 두고 그것으로 찾는다 — 원문은 메일 본문에만 있고, 서버는 링크를 다시 만들 수 없다. 맞혀 볼 수 없는 값이라 느린 해시는 필요 없다.
|
||||
- 경로에 둔다(`/s/<토큰>` · `/api/public-links/<토큰>`) — 받는 쪽 보안 장비의 링크 바꿔 쓰기와 잘 맞는다. 로그에서는 가린다(아래 "로그").
|
||||
- 모양(`^[A-Za-z0-9_-]{43}$`)이 아니면 DB 에 묻지 않고 404.
|
||||
|
||||
**상태별 응답**
|
||||
|
||||
| 상황 | 링크 정보 `GET /api/public-links/{token}` | 파일 `GET /api/public-links/{token}/files/{fileId}` |
|
||||
| --- | --- | --- |
|
||||
| 모양이 틀린 · 모르는 토큰, 보내기를 취소한 링크, 기록을 지운 링크(끝난 뒤 90일), 그 링크에 없는 파일 | 404 `RES_001` | 404 `RES_001` |
|
||||
| 기간 지남 | 200 `status: 'expired'` (`files: []`) | 410 `FILE_007` |
|
||||
| 링크를 끊음(보낸 사람 · 운영) | 200 `status: 'revoked'` (`files: []`) | 410 `FILE_007` |
|
||||
| 검사 중 | 200 — `scanning` | 409 `FILE_008` |
|
||||
| 위험 · 횟수 초과 · 찾을 수 없음 | 200 — `infected` · `limit-reached` · `missing` | 410 `FILE_007` |
|
||||
| 받을 수 있음 · 검사하지 못함 | 200 — `available` · `unscannable` | 200 · 206 · 416 |
|
||||
| 요청 수 한도 | 429 `REQ_001` | 429 `REQ_001` |
|
||||
|
||||
- 없는 링크와 끝난 링크를 가려 알려도 된다 — 토큰을 가진 사람에게만 보이는 구분이다.
|
||||
- 404 · 409 · 410 은 표준 실패 형식이지만 전역 필터를 거치지 않고 컨트롤러가 쓴다 — 로그는 debug 만(끝난 링크를 누르는 받는 사람 · 봇이 잦아 warn 이면 알림이 쌓인다 —
|
||||
외부 이미지 중계와 같은 결 `mail-image.controller.ts:64-67`). 5xx 는 지금처럼 error.
|
||||
구현(2026-10-30): 공개 컨트롤러에만 건 같은 예외 필터(`HttpExceptionFilter` 의 `quietStatuses` — 404 · 409 · 410 은 debug)가 쓴다 — 형식 · traceId 는 다른 API 와 같다.
|
||||
- 링크 정보 응답도 `Cache-Control: no-store` · `X-Robots-Tag: noindex` · `Referrer-Policy: no-referrer`.
|
||||
|
||||
**파일 응답** (공개 받기 · 우리 웹메일 받기 공용)
|
||||
- **머리글**: `Content-Type: application/octet-stream`(늘 — 형식은 서버가 정한다) · `Content-Disposition: attachment; filename=…; filename*=UTF-8''…`(일반 첨부와 같은 `buildAttachmentDisposition` — `shared/utils/content-disposition.ts:39`) ·
|
||||
`X-Content-Type-Options: nosniff` · `Content-Security-Policy: default-src 'none'; sandbox` · `Cache-Control: private, no-store` · `Referrer-Policy: no-referrer` · `X-Robots-Tag: noindex` ·
|
||||
`Accept-Ranges: bytes` · `Content-Length` · `ETag: "<fileId>"`(강한 — 파일 내용은 id 마다 바뀌지 않는다) · `Last-Modified`(다 올린 때) · `X-Accel-Buffering: no`.
|
||||
- **범위(Range)**:
|
||||
- 없으면 200 전체. `bytes=` 범위 하나면 206 + `Content-Range: bytes <처음>-<끝>/<크기>`(끝이 크기를 넘으면 맞춰 자르고, `bytes=-N` 은 끝 N 바이트).
|
||||
- 범위가 여럿이거나 형식이 틀리면 범위를 무시하고 200 전체(RFC 9110 이 허락한다). `If-Range` 가 ETag · Last-Modified 와 다르면 200 전체.
|
||||
- 처음이 크기 이상이면 416 + `Content-Range: bytes */<크기>` — 본문은 비운다(내려받기 관리자가 읽는 응답이라 표준 실패 형식이 아니다 — 2절 첨부 받기처럼 래핑 예외).
|
||||
- `HEAD` 는 같은 머리글만 준다(파일을 열지 않고 세지 않는다).
|
||||
- **횟수**:
|
||||
- 센다: `GET` 이고 0번째 바이트부터 보내는 응답(범위 없음 · 무시 · `bytes=0-…`). 머리글을 보내기 직전에 원자적으로 더한다 — 상한에 닿았으면 더하지 않고 410 `FILE_007`.
|
||||
- 세지 않는다: 이어 받기(범위의 처음 > 0), `HEAD`, 링크 정보(화면 열기), 실패 응답, 링크를 만든 사람이 우리 웹메일 경로로 받은 것.
|
||||
- 상한(`SHARE_DOWNLOAD_LIMIT`, 파일당 500)에 닿으면 그 파일은 이어 받기도 410 `FILE_007` 이다. 남은 횟수는 어디에도 내세우지 않는다.
|
||||
- 받은 수 · 마지막으로 받은 때는 보낸 사람만 본다(목록 · 읽기 화면의 `downloads`). 누가 · IP 는 남기지 않는다.
|
||||
- 내려받는 동안 링크를 끊거나 기간이 지나도 이미 시작한 응답은 끝까지 간다(다음 요청부터 막힌다). 서버 종료가 시작되면 진행 중인 내려받기를 바로 끊고(종료 유예 30초를 붙잡지 않게 — 실시간 알림의 종료와 같은 결) 그 뒤에 온 받기는 시작하지 않는다 — 브라우저가 범위로 이어 받는다.
|
||||
- ⚠️ Nest 의 `StreamableFile` 로는 206 · 416 을 보낼 수 없다(`@Res({ passthrough: true })` 면 라우트 상태로 덮어쓴다 — `@nestjs/platform-express` 11.2.5) —
|
||||
처리기가 `@Res()` 로 상태 · 머리글을 직접 쓰고 흘려보낸다(2절 첨부 받기 `message.controller.ts:357-381` 과 다른 길).
|
||||
|
||||
**요청 수 한도**: IP 마다(`clientIpKey()` — IPv6 는 /64) 링크 정보 1분 60회, 파일 1분 300회(`HEAD` 포함 — 내려받기 관리자가 범위를 나눠 여러 번 부른다). 넘으면 429 `REQ_001`.
|
||||
요청 수일 뿐 바이트는 세지 않는다 — 퍼진 링크의 대역폭은 횟수 상한 · 기간 · 끊기가 막는다(앞단 동시 연결 — IP 마다 공개 10 · 앱 안 30, 전체 400 · 200 — 아래 "운영 확인").
|
||||
|
||||
**화면 `/s/{token}`** — SPA 의 공개 라우트
|
||||
- 셸 · 로그인 없이 그린다 — 라우트의 공개 표시(`meta.public`)로 세션 복원(`/auth/me` · `/auth/refresh` — 지금 가드는 모든 이동 전에 부른다 `frontend/src/router/index.ts:26-28`)과 로그인 화면 보내기를 건너뛴다.
|
||||
API 는 세션 갱신 · 세션 만료 대화상자를 타지 않는 따로 된 요청(쿠키 없이)으로 부른다.
|
||||
- 보이는 것: 회사 표시, 보낸 사람 이름 · 주소, 보낸 날 · 끝나는 날(한국 시간), 파일 목록(이름 · 크기 · 검사 상태 · [내려받기]), "이 화면은 로그인이나 비밀번호를 묻지 않습니다", 신고 · 문의 주소, 영어 한 줄.
|
||||
상태: 정상 · 검사 중 · 위험 · 검사하지 못함 · 만료 · 회수 · 횟수 초과 · 없는 링크 · 요청이 너무 많음, 좁은 폭(휴대폰). 글 · 모양은 디자인 README "11단계".
|
||||
- [내려받기]는 `<a href>` 이동이다(큰 파일을 `fetch` 로 받으면 메모리에 다 오른다) — 누르기 직전에 링크 정보를 한 번 더 묻고, 받을 수 없게 됐으면 화면에서 알린다(이동 뒤 JSON 오류가 뜨지 않게).
|
||||
끊기면 브라우저의 다시 시도로 이어 받는다. 검사 중이면 화면이 잠시 뒤 다시 묻는다(간격은 구현하며 정함 — 위 요청 수 안에서).
|
||||
|
||||
**공개 호스트** (`SHARE_PUBLIC_URL`)
|
||||
- 운영은 `https://files.arasystem.it.kr` — 포털과 다른 호스트라 털린 계정이 퍼뜨린 악성 파일로 포털까지 차단 목록에 오르는 위험을 어느 정도 떼어 놓고, 포털 쿠키도 가지 않는다
|
||||
(외부 리버스 프록시가 포털과 같은 사설 주소:8080 으로 넘긴다).
|
||||
- 공개 호스트에서는 **화면 `/s/{token}`(과 그 자산) · `/api/public-links/…` 만** 연다 — 그 밖은 404. 포털 호스트에서는 `/s/…` · `/api/public-links/…` 를 열지 않는다(404). 둘이 같은 주소면(개발) 둘 다 연다.
|
||||
앞단 nginx(호스트마다 server)와 백엔드(요청의 `Host` — nginx 가 원래 값을 넘긴다, `frontend/nginx.conf` 의 `proxy_set_header Host $host`)가 함께 막는다(구현하며 정함).
|
||||
구현(2026-10-30 — 백엔드): 요청 추적 ID 바로 뒤의 미들웨어(`modules/file-sharing/public-host.ts`)가 `Host` 의 호스트 이름(포트 없이 · 소문자 · 끝의 점 없이)을 `SHARE_PUBLIC_URL` 의 호스트 이름과 견준다.
|
||||
공개 호스트이면 `/api/public-links/` 만, 아니면 그 경로만 404 `RES_001`(표준 실패 형식 · 로그 없음 — 경로의 대소문자는 가리지 않는다).
|
||||
경로는 스택이 읽을 수 있는 모든 모양 — 원문(`?` 앞) · 라우터가 쓰는 경로(Express 의 `req.path`: 절대 주소 꼴은 스킴 · 호스트를 떼고, `#` 이 섞이면 `\` 를 `/` 로 읽는다) ·
|
||||
WHATWG URL(`\` → `/`, `%2e` 조각) · 퍼센트 인코딩을 한 번 푼 원문(nginx 처럼 `%2F` 도) — 을 정규화(겹친 `/` · `.` · `..` 조각 · 대소문자)해 본다.
|
||||
공개 호스트는 모든 모양이 공개 경로일 때만 열고(그래서 절대 주소 꼴은 받지 않는다), 포털은 어느 모양이라도 공개 경로면 닫는다. 읽지 못한 모양이 있으면 어느 쪽도 열지 않는다
|
||||
(nginx 의 정규화에 기대지 않는다 — 2026-10-30, 처음 판은 포털 Host 의 절대 주소 꼴 · `\…#` 요청이 공개 컨트롤러에 닿았다). 공개 호스트 이름이 포털의 출처(`CORS_ORIGIN`, 비운영은 개발 기본 주소 포함)의
|
||||
호스트 이름과 같으면(개발) 가르지 않는다 — 포트만 다른 두 주소는 nginx 가 포트 없는 `Host` 를 넘겨 가를 수 없기 때문이다.
|
||||
- 공개 주소는 `CORS_ORIGIN` 에 넣지 않는다 — 공개 화면은 같은 호스트의 공개 API 만 부른다(`connect-src 'self'` 그대로). 포털 화면이 링크의 상태를 볼 때는 포털의 `POST /api/share-link-lookups` 를 쓴다.
|
||||
- 덩이 · `url` 은 늘 지금의 `SHARE_PUBLIC_URL` 로 만든다. 바꾸면 그 전 메일의 덩이를 알아보지 않고, 옛 주소는 옛 호스트가 남아 있어야 열린다 — 바꾸지 않는다.
|
||||
|
||||
### 기간 · 보관 · 정리
|
||||
|
||||
- **링크가 끝나는 때** = 보낸 때(예약은 `sendAt`)의 **한국 날짜에 30일을 더한 날의 23:59:59.999(한국 시간 — UTC+9, 일광 절약 없음)**.
|
||||
예: 2026-10-30 에 보내면 `expiresAt` 은 `2026-11-29T14:59:59.999Z`(11월 29일(일) 끝까지). 받는 사람 모두 같은 날짜이고, 덩이 · 화면은 그 한국 날짜 하나로 적는다.
|
||||
요청마다 `지금 ≤ expiresAt` 으로 본다 — 정리 작업이 늦어도 정확하다.
|
||||
|
||||
| 대상 | 남는 기간 | 그 뒤 |
|
||||
| --- | --- | --- |
|
||||
| 올리는 중 | 마지막 조각(없으면 만든 때) 뒤 24시간 — 임시 저장에 실려도 같다 | 지운다 — `GET` 404, 임시 저장본에서는 `MissingLargeFile`(`expired`) |
|
||||
| 다 올렸지만 어디에도 실리지 않은 파일 | 다 올린 뒤 24시간 | 지운다 |
|
||||
| 임시 저장에 한 번이라도 실린 파일 | 다 올린 날(한국)부터 위와 같은 셈으로 30일째 날 끝 | 지운다 — 임시 저장본에서는 `MissingLargeFile`(`expired` — "기간이 지나 다시 올려야 하는 파일") |
|
||||
| 사용자가 지운 보내지 않은 파일 | 곧(보관량에서는 그때 바로 빠진다) | 임시 저장본에서는 `MissingLargeFile`(`deleted` — "지운 파일") |
|
||||
| 보낸 파일 | 그 파일을 실은 살아 있는 링크가 끝나는 때 가운데 가장 늦은 것 (임시 저장 기간보다 늦으면) | 지운다 |
|
||||
| 끊은 링크의 파일 · 위험 판정 파일 | 다른 쓰임이 없으면 곧 | 지운다 — 보관량에서는 그때 바로 빠진다 |
|
||||
| 링크 기록 | 끝나거나 끊긴 뒤 90일 — 그동안 공개 화면은 "기간 지남" · "공유 멈춤"이고 목록에 남는다 | 지운다 → "없는 링크"(404), 목록에서 사라진다 |
|
||||
| 취소한 링크 (보내기 취소 · 보내지 못함) | 되돌린 초안이 쓰는 동안 — 90일 | 지운다 |
|
||||
| 파일 기록 (이름 · 크기) | 그 파일을 가리키는 링크 기록이 다 지워질 때까지. 보낸 적 없는 파일은 지운 뒤 30일 | 지운다 — 그 뒤 임시 저장본은 그 id 를 버린다 |
|
||||
|
||||
- 임시 저장본을 영구 삭제해도 파일은 위 기간까지 남는다 — 초안은 우리 API 밖(IMAP · 자동 비우기)에서도 지워져 서버가 다 알 수 없다(알려진 한계 — 보관량에 잡히고, 보내지 않은 파일 목록에서 지울 수 있다).
|
||||
- 받은 기록은 **받은 수 · 마지막으로 받은 때만** 둔다 — 누가 · IP · 브라우저는 남기지 않는다.
|
||||
- 정리 작업(cron)이 바이트를 지운다 — 기록을 먼저 바꾸고 실제로 바꾼 것만 지운다(서명 그림 정리와 같은 결). 기록에 없는 파일은 하루 지난 것만 지운다(구현하며 정함).
|
||||
구현(2026-10-30): 파일 정리는 **한 시간마다**(`file-storage.cleanup.ts`) — 기간이 지난 파일을 지움(기간 지남)으로 바꾸고 바이트를 지운다, 지웠거나 위험 판정인데 바이트가 남은 것을 지운다,
|
||||
끝내기를 시작한 지 10분 넘게 멈춘 올리기를 되살린다(시작한 때 `completion_started_at` 로 고른다 — 다 옮겼으면 다 올림, 아니면 올리는 중), 지운 지 30일 지난 파일 기록을 지운다, 기록에 없는 저장소 파일 가운데 하루 지난 것을 지운다.
|
||||
링크 정리는 **하루 한 번 05:00(서버 시각)**(`file-sharing.cleanup.ts`) — 끝나거나 끊기거나 취소한 지 90일 지난 링크 기록을 지운다. 링크 기록은 파일 이름 · 크기를 따로 두므로 파일 기록과 따로 지운다.
|
||||
- **백업**: 파일 바이트는 백업하지 않는다(30일짜리 전송물) — 기록은 DB 백업에 든다. 복원한 뒤 바이트가 없는 파일은 `missing` 이다.
|
||||
|
||||
### 악성 검사 (ClamAV)
|
||||
|
||||
- **언제**: 끝내기 직후 검사 차례에 넣는다 — 서버 전체에서 한 번에 1~2개, 먼저 끝낸 것부터(구현하며 정함). 2GB 는 분 단위가 걸릴 수 있다.
|
||||
구현(2026-10-30): 한 번에 **2개**, 먼저 끝낸 것부터. 차례는 DB(`scan_status = 'pending'` · `scan_next_at`)라 서버를 다시 띄워도 잇는다 — 다 올린 직후 · 30초마다 · 검사 하나가 끝날 때 깨운다.
|
||||
clamd 연결은 10초, 흘려보내는 동안 무소식 10분, 다 보낸 뒤 판정까지 30분 + 보낸 1MB 마다 1초(2,047MB 면 3,847초 ≈ 64분 — `file-storage.constants.ts` 의 `clamavVerdictTimeoutMs`)를 넘으면 끊고 미뤄서 다시 한다.
|
||||
30분은 크기와 상관없는 몫이다 — 7z · tar.gz 는 MaxScanTime 없이 MaxScanSize(4,094MB)까지 풀리고, 실측 약 7MB/s 로 10분 · 운영 CPU 가 2~3배 느리면 20~30분이다. 1MB 마다 1초는 파일 자체의 몫(실측의 약 7배)이다.
|
||||
판정이 끝내 오지 않아 같은 파일을 끝없이 다시 검사하는 것보다 한 번 오래 기다리는 편을 골랐다(2026-10-30).
|
||||
연결에는 TCP keepalive 를 켠다(60초 뒤부터 1초 간격 10번 — Node 24 · libuv 1.52): 컨테이너째 사라져 RST 도 오지 않는 clamd 를 약 70초 안에 `read ETIMEDOUT` 으로 끊고 다른 끊김처럼 미뤄서 다시 한다.
|
||||
살아 있는 채 멈춘 clamd 는 keepalive 로 알 수 없어 판정 대기까지 검사 자리를 잡는다.
|
||||
- **검사 중에도 보낸다** — 받는 쪽은 `scanning`(받기 409 `FILE_008`)이고, 끝나면 받는다.
|
||||
- **판정** — 백엔드가 clamd 에 `INSTREAM` 으로 흘려보낸다(규약을 직접 구현 — 새 의존성 없음):
|
||||
- `OK` → `clean`(받을 수 있음).
|
||||
- `FOUND` 가운데 암호 · 검사 한도 표시(`Heuristics.Encrypted.*` · `Heuristics.Limits.Exceeded.*`) → `unscannable`(받을 수 있음 · "검사하지 못함").
|
||||
ClamAV 1.4.6 실측(2026-10-30): 암호 zip(ZipCrypto · AES) `Heuristics.Encrypted.Zip` · 암호 7z `.7Zip` · 사용자 암호 pdf `.PDF`(소유자 암호만 있는 pdf 는 `OK`),
|
||||
20겹 zip `Heuristics.Limits.Exceeded.MaxRecursion` · 파일 12,000개 zip `.MaxFiles` · 풀면 수 GB 인 zip `.MaxScanTime` · 한 파일 한도를 넘는 파일 `.MaxFileSize`.
|
||||
같은 압축 안에 진짜 서명과 휴리스틱(암호 · 한도)이 함께 있으면 진짜 서명이 나온다(`infected`).
|
||||
- 그 밖의 `FOUND` → `infected`(EICAR 는 `Eicar-Test-Signature FOUND` — 압축 안에 있어도 같다).
|
||||
- 스트림 크기 한도 오류(`INSTREAM size limit exceeded. ERROR` — clamd 의 `StreamMaxLength` 가 파일보다 작다) → **`pending` 그대로 + error 로그**(설정 오류 — 아래 "닿지 못함"과 같이 다시 시도).
|
||||
처음 초안의 `unscannable` + warn 은 2026-10-30 리뷰로 바꿨다 — 운영에서 `StreamMaxLength` 를 올리는 것을 잊으면 20MB 를 넘는 모든 파일이 검사 없이 열리고 흔적은 warn 한 줄뿐이었다.
|
||||
clamd 는 이 글을 보낸 뒤 연결을 닫고(FIN), 백엔드가 계속 보내면 RST 다 — 백엔드가 글을 읽기 전에 RST 가 오면 판정 글 없이 `EPIPE` 로 끝나 아래 "끊음"으로 다룬다(세 번째부터 error).
|
||||
운영 설정은 `StreamMaxLength` = 한 파일 한도의 최대값(2047M)이라 정상에서는 일어나지 않는다.
|
||||
- clamd 가 판정 글을 보내기 전에 연결을 끊음(RST · 빈 응답) → 다시 시도(`pending`).
|
||||
- **검사기에 닿지 못함** · 시간 초과 · 그 밖의 오류 → `pending` 그대로 두고 간격을 늘려 다시 한다(1시간마다까지 — 구현하며 정함). 이어서 실패하면 error 로그(운영 알림).
|
||||
구현(2026-10-30): 1분부터 두 배씩 1시간까지, 세 번째 실패부터 error(그 전은 warn). 판정 이름은 `Heuristics.Encrypted…` · `Heuristics.Limits.Exceeded…` 로 시작하면 `unscannable` 이다 —
|
||||
판정 글 견주기는 `modules/file-storage/clamav-client.ts` 의 `CLAMAV_REPLY` · `classifyClamavReply` 한 곳이고, `\0` 까지 받은 글만 판정으로 본다(끝나지 않은 글은 끊김 — 다시 시도).
|
||||
그 로그에는 단계와 보낸 바이트 수가 남는다 — 시도마다 같은 "흘려보내는 중(보낸 N바이트)"이면 크기 한도를 의심한다. 판정 글이 없으면 크기 한도와 clamd 재시작을 가를 수 없어 설정 오류로 단정하지 않는다(2026-10-30 확인).
|
||||
2026-10-30 release-ops 가 실제 clamd(ClamAV 1.4.6 LTS · 운영 설정)로 위 이름들을 재어 이 견주기와 맞음을 확인했다 — 판 · 설정을 바꾸면 다시 잰다(실측표는 mail/README.md "악성 검사 (ClamAV)").
|
||||
**판정 없이 받을 수 있게 되는 일은 없다**(닫힌 쪽).
|
||||
- **위험**: 받기를 막고(410 `FILE_007`) · 보내기를 막고(`MAIL_016`) · 보낸 사람에게 알린다 — 목록 상태 + 실시간 알림 `large-file` 이벤트(아래). 바이트는 곧 지우고(보관량에서 바로 빠진다) 기록(이름 · 판정 이름)은 남긴다.
|
||||
- **검사하지 못함**: 막지 않는다 — 공개 화면 · 목록 · 읽기 화면에 "검사하지 못함" 글자.
|
||||
- 검사는 한 번이다 — 나중에 나온 서명으로 다시 보지 않는다(알려진 한계).
|
||||
- 검사기를 쓰지 않는 개발(`CLAMAV_HOST` 비움)은 끝내자마자 `unscannable` 이다. 운영은 `CLAMAV_HOST` 가 없으면 기동하지 않는다.
|
||||
- **실시간 알림**(2절 `GET /api/mail-events` 에 더한다) — 위험 판정 때 그 직원의 연결에 보낸다:
|
||||
|
||||
```text
|
||||
event: large-file
|
||||
data: {"fileId":"…","name":"견적서.zip","scanStatus":"infected"}
|
||||
```
|
||||
|
||||
클라이언트는 알림("올린 대용량 파일 ‘견적서.zip’에서 위험한 내용이 발견되어 막았습니다." + [대용량 첨부 보기])을 띄우고, 쓰기 화면에 그 파일이 있으면 "위험 — 보낼 수 없음"으로 바꾼다.
|
||||
연결이 없던 직원은 목록 상태로만 안다(정한 것 — 2026-10-30, 다음에 들어올 때 따로 알리지 않는다).
|
||||
- 운영(release-ops): clamd 의 `StreamMaxLength` · `MaxFileSize` · `MaxScanSize` 를 한 파일 한도 이상으로, `AlertEncrypted` · `AlertExceedsMax` 를 켠다. 메모리 3~4GiB(운영 메일 서버 8GB), 서명 갱신(freshclam)의 바깥 연결,
|
||||
**backend 와만 잇는 새 내부 네트워크**(`app-network` 가 아니다 — mail setup 의 구성원 검사에 걸리고 메일 엔진의 신뢰 대역에 든다).
|
||||
구현(2026-10-30): 컨테이너 둘 — `clamav`(clamd · `<PROJECT_NAME>_clamav` internal 네트워크에만 · 서명 볼륨 읽기 전용)과 `clamav-updater`(freshclam · 자기 바깥 길 `clamav-egress` 에만).
|
||||
`StreamMaxLength` = `MaxFileSize` = 2047M · `MaxScanSize` 4094M · `MaxScanTime` 5분(1.4 에서는 ZIP 에만 걸린다). clamd 메모리 1GB 안팎(한도 2g — 서명을 다시 읽을 때 두 벌을 들지 않게 `ConcurrentDatabaseReload no`, 그동안 30초 안팎 검사가 기다린다).
|
||||
2,047MB 무작위 파일도 끝까지 검사한다(`OK` — 다 보낸 뒤 296초, 코어 하나 · 개발 PC). 설정 `mail/clamav/`, 운영 절차 · 메모리 예산 mail/README.md "대용량 첨부 (11단계 — 인프라)".
|
||||
|
||||
### 환경 변수 (루트 env — 넣는 일은 release-ops, 검증 · 테스트는 backend `env.validation.ts`)
|
||||
|
||||
| 키 | 기본값 | 범위 · 검사 | 뜻 |
|
||||
| --- | --- | --- | --- |
|
||||
| `SHARE_PUBLIC_URL` | 비운영에서 비우면 `CORS_ORIGIN` 의 첫 주소 | 출처만(스킴 · 호스트 · 포트 — 경로 · 끝 `/` 없음). **운영 필수** · `https` · 로컬 주소 · 예시 도메인 금지(`ALLOW_LOCALHOST_URL` 예외 — `CORS_ORIGIN` · `BACKEND_API_URL` 과 같은 검사). 모든 환경: 앞단 nginx(`frontend/nginx-env.envsh` 의 `nginx_env_origin_host`)가 받는 모양만 — 스킴 소문자, 호스트는 영문 · 숫자 · `.` · `-`(점 · 하이픈으로 시작하거나 끝나지 않고 점이 이어지지 않게 — 끝 점 없이), 포트는 0 으로 시작하지 않는 숫자, 사용자 정보 · 대괄호(IPv6) · 앞뒤 공백 없이(다르면 백엔드만 뜨는 반쪽 배포) | 링크 · 덩이 · 덩이 알아보기 · 공개 호스트 막기. 운영 `https://files.arasystem.it.kr` |
|
||||
| `SHARE_REPORT_ADDRESS` | `postmaster@<MAIL_DOMAIN>` | 메일 주소 | 공개 화면의 신고 · 문의 |
|
||||
| `SHARE_DOWNLOAD_LIMIT` | 500 | 1 ~ 100,000 | 파일당 내려받기 상한(받기 시작한 수) |
|
||||
| `MAIL_LARGE_FILE_MAX_SIZE_MB` | 2047 | 1 ~ 2047, `MAIL_ATTACHMENT_MAX_SIZE_MB` 이상 | 한 파일 |
|
||||
| `MAIL_LARGE_FILE_MAX_COUNT` | 10 | 1 ~ 20 | 메일 한 통 |
|
||||
| `MAIL_LARGE_FILE_USER_QUOTA_GB` | 10 | 1 ~ 1,000 | 직원당 보관량 |
|
||||
| `MAIL_LARGE_FILE_RETENTION_DAYS` | 30 | 1 ~ 90 | 보낸 뒤 · 임시 저장의 보관 날 수 |
|
||||
| `FILE_STORAGE_MAX_TOTAL_GB` | 비움 = 한도 없음(여유만 본다) | 1 이상 | 서버 전체 보관량 |
|
||||
| `FILE_STORAGE_MIN_FREE_GB` | 10 | 1 이상 | 만든 뒤에도 남길 디스크 여유 |
|
||||
| `CLAMAV_HOST` | 비움 = 검사하지 않음(개발) | **운영 필수** | clamd 주소 |
|
||||
| `CLAMAV_PORT` | 3310 | 1 ~ 65,535 | clamd 포트 |
|
||||
| `FILE_STORAGE_DIR` | `/app/files` | compose 가 고정해 넣는다(루트 env 아님) | 저장 위치 — named volume `<PROJECT_NAME>_files` |
|
||||
|
||||
- 숫자는 앞자리 0 없는 10진수다(`isPositiveIntString` — 지금 규칙). compose 가 넣는 빈 문자열은 기본값이다(`parsePositiveInt`). nginx 는 이 값들을 읽지 않는다(조각 17MB 는 계약 상수).
|
||||
- 구현 때 더한 검사(2026-10-30 — `env.validation.ts`): `MAIL_LARGE_FILE_USER_QUOTA_GB` × 1,024 는 `MAIL_LARGE_FILE_MAX_SIZE_MB` 이상이어야 한다(한도까지 큰 파일 하나는 올릴 수 있게).
|
||||
`MAIL_LARGE_FILE_MAX_SIZE_MB` 와 `MAIL_ATTACHMENT_MAX_SIZE_MB` 는 앞의 값을 정했을 때만 견준다. `CLAMAV_HOST` 는 호스트 이름만(스킴 · 포트 없이). `SHARE_REPORT_ADDRESS` 는 메일 주소.
|
||||
운영에서 `SHARE_PUBLIC_URL` 의 호스트 이름이 `CORS_ORIGIN` 의 어느 호스트 이름과 같으면(포트만 다르거나 대소문자 · 끝의 점만 달라도) 기동하지 않는다 — 공개 호스트 막기가 소리 없이 꺼지므로(2026-10-30 리뷰).
|
||||
공개 주소는 `CORS_ORIGIN` 에 넣지 않는다. 로컬에서 운영 이미지를 검증할 때도 이름을 나눈다(예: `localhost` 와 `127.0.0.1`).
|
||||
`SHARE_PUBLIC_URL` 의 모양은 모든 환경에서 앞단 nginx 와 같은 규칙으로 본다(위 표 — 2026-10-30, 대문자 스킴 · 끝 점 · 앞뒤 공백도 거절).
|
||||
`FILE_STORAGE_DIR` 를 compose 가 넣지 않으면 작업 폴더의 `files`(컨테이너에서는 `/app/files`, 호스트 개발에서는 `backend/files` — gitignore)다.
|
||||
|
||||
### 로그
|
||||
|
||||
- **토큰 · 링크 주소는 남기지 않는다**: 백엔드는 요청 주소의 `/api/public-links/<토큰>` 을 길이만 남긴다(`maskUrlForLog` 에 더한다 — 지금은 `/api/mail-images/<표>` 만, `mask.ts:26`). 경로의 대소문자는 가리지 않는다(라우팅이 가리지 않는다).
|
||||
구현(2026-10-30): 경로 어디서든(`//api/…` · `/api//…` · `/api/./…` 도) `public-links` 조각 뒤의 조각을 길이만 남긴다 — `files` 와 파일 id(UUID)는 남긴다.
|
||||
`POST /api/share-link-lookups` 의 본문(주소)도 남기지 않는다 — 개수만.
|
||||
앞단 nginx 접근 로그도 요청 줄 · Referer 의 토큰을 가린다(쿼리는 원래 빼고 — `frontend/nginx.conf` 의 `log_format main_rid` 앞 map 들. 2026-10-30 리뷰로 경로 모양이 아니라 토큰 모양(43자 base64url)으로도 가린다 — `//s/` · `%73` 같은 변형. 요청 줄에서는 둘까지 가리고, 43자가 아닌 값(UUID · 메일 id)은 그대로다. 토큰 안을 퍼센트 인코딩으로 쪼갠 꼴은 가리지 못한다 — 한계).
|
||||
nginx 오류 로그(502 · 504 등)는 지금 정책대로 요청 줄을 그대로 남긴다 — 공개 내려받기의 그 줄에는 토큰이 남는다(운영 담당만 읽는다). 외부 리버스 프록시도 같게(운영 확인).
|
||||
- 파일 이름은 남기지 않는다(직원의 글이다) — 파일 · 링크 id 와 크기만. 직원 주소는 가린다(`maskEmail`).
|
||||
- 받는 쪽 IP 는 남기지 않는다 — 요청 수 한도의 셈(메모리)에만 쓴다. 앞단 nginx 접근 로그의 IP 는 지금 운영 로그 정책 그대로 — 바깥 사람의 개인 정보일 수 있어 보관 기간을 운영에서 정한다.
|
||||
- 레벨: 링크 만들기 · 끊기 · 다 올림 · 보내지 않은 파일 지우기는 log, 위험 판정은 warn(판정 이름 · 파일 id), 조각 · 내려받기 성공은 남기지 않는다(반복되는 정상 흐름), 공개 경로의 404 · 409 · 410 은 debug,
|
||||
검사기 장애가 이어지거나 디스크가 모자라면 error.
|
||||
|
||||
### 에러 코드 (11단계에 더함 — 제안, 문구는 레지스트리에서 정한다)
|
||||
|
||||
| 코드 | HTTP | 뜻 | 쓰는 곳 | 문구 (제안) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `FILE_003` | 409 | 올리기 · 파일의 지금 상태와 맞지 않는 요청 — 끝낸 올리기에 조각, 같은 조각을 동시에, 받은 조각과 내용이 다른 조각, 빠진 조각이 있는데 끝내기, 끝내기가 진행 중, 살아 있는 링크에 실린 파일 지우기. 아무것도 바꾸지 않았다 | 조각 · 끝내기 · 지우기 | "파일 올리기 상태가 맞지 않습니다. 다시 시도해 주세요." |
|
||||
| `FILE_004` | 400 | 조각의 `Content-Digest` 가 받은 바이트와 다르다 — 그 조각을 받지 않았다(같은 조각을 다시) | 조각 | "파일 조각이 보내는 중에 손상되었습니다. 다시 시도해 주세요." |
|
||||
| `FILE_005` | 409 | 직원 보관 한도(보관량 · 보내지 않은 파일 수)를 넘는다 — 만들지 않았다 | 만들기 | "대용량 첨부 보관 한도를 넘어 더 올릴 수 없습니다. 보낸 파일의 링크를 끊거나 보내지 않은 파일을 지우면 공간이 생깁니다." |
|
||||
| `FILE_006` | 507 | 서버 저장 공간이 모자란다 — 서버 총량 · 디스크 여유 · 쓰다 `ENOSPC` | 만들기 · 조각 | "서버의 저장 공간이 모자라 지금은 파일을 올릴 수 없습니다. 잠시 뒤 다시 시도해 주세요." |
|
||||
| `FILE_007` | 410 | 더 내려받을 수 없는 파일 — 기간 지남 · 공유 멈춤 · 횟수 초과 · 위험 · 찾을 수 없음 | 공개 받기 · 우리 웹메일 받기 | "이 파일은 더 내려받을 수 없습니다. 필요하면 보낸 사람에게 문의해 주세요." |
|
||||
| `FILE_008` | 409 | 아직 보안 검사 중인 파일 — 끝나면 받는다 | 공개 받기 · 우리 웹메일 받기 | "보안 검사가 끝나지 않은 파일입니다. 잠시 뒤 다시 시도해 주세요." |
|
||||
| `MAIL_016` | 400 | 보내거나 저장할 대용량 파일을 쓸 수 없다 — 없음 · 다른 직원 것 · 지움 · 기간 지남, (보낼 때) 올리는 중 · 위험. 메일은 나가지 않았다 | 발송 · 임시 저장 | "대용량 첨부 파일을 보낼 수 없습니다. 해당 파일을 빼거나 다시 올린 뒤 보내 주세요." |
|
||||
|
||||
- 문구에 숫자를 넣지 않는다 — 한도가 env 로 바뀐다(`MAIL_006` · `CONTACT_002` 처럼 상수와 견주는 테스트를 둘 수 없다).
|
||||
- 그 밖은 지금 코드를 쓴다 — 형식 400 `FILE_001`, 한 파일 · 조각 본문 413 `FILE_002`, 개수 · 모양 400 `VAL_001`, 없음 404 `RES_001`, 요청 수 · 동시 수 429 `REQ_001`, 엔진 503 `MAIL_001`.
|
||||
- 410 · 507 은 상태 역매핑(`error-codes.ts` 의 `STATUS_TO_ERROR_CODE`)에 넣지 않는다 — 이 코드들은 `BusinessException` 으로만 나온다. 앞단 nginx 는 백엔드의 4xx · 5xx 를 손대지 않는다(`proxy_intercept_errors` 꺼짐).
|
||||
- 세 클라이언트의 목록 테스트에는 클라이언트가 실제로 분기하는 코드만 더한다(웹: `FILE_003` · `FILE_004` · `FILE_005` · `FILE_006` · `MAIL_016` — 2026-10-30. `FILE_008` 은 웹이 분기하지 않는다 — 받기는 `<a href>` 이동이고 검사 중이면 화면이 먼저 막는다).
|
||||
|
||||
### 운영 확인 (이 계약이 기대하는 것 — 고치는 일은 release-ops)
|
||||
|
||||
- 앞단 nginx: `/api/file-uploads` 본문 17MB · 읽기 90초. `/api/public-links/` · `/api/messages/*/large-files/` 는 응답 버퍼링을 끄고 `X-Accel-Buffering` 을 외부로 넘긴다
|
||||
(기본값이면 느린 받는 사람마다 임시 파일을 1GB 까지 만든다 — 실시간 알림과 같은 방법). `/s/` 는 `index.html` + `Cache-Control: no-cache` · `X-Robots-Tag: noindex, nofollow` · `Referrer-Policy: no-referrer`.
|
||||
접근 로그의 경로 토큰 가리기, 공개 호스트의 server(위 "공개 호스트").
|
||||
**내려받기 동시 연결 상한**(2026-10-30 리뷰 — 버퍼링을 끈 느린 받기는 연결 하나가 nginx 연결 둘과 백엔드 fd 를 잡아, 토큰 하나로 프론트 nginx 를 바닥낼 수 있다): `limit_conn` 으로
|
||||
공개 받기 IP 마다 10 · 앱 안 받기 IP 마다 30(사무실 NAT) · 서버 전체 공개 받기 400 · 앱 안 받기 200, 넘으면 429 `REQ_001`(nginx 오류 JSON). `worker_connections` 는 이 수와 SSE · 포털을 함께 버티게.
|
||||
전체 상한을 경로마다 따로 둔다(2026-10-30 리뷰 — 처음 판은 두 경로가 500 을 함께 써, 바깥 IP 50개(IPv6 면 /64 하나)로 직원의 앱 안 받기가 모두 429 가 됐다).
|
||||
경로의 대소문자 변형(`/FILES/` · `/api/Messages/…`)도 같은 상한 · 같은 흘려보내기를 탄다(2026-10-30 리뷰 — 처음 판은 글자 하나로 상한 · `X-Accel-Buffering` 넘김을 비켜 갔다).
|
||||
구현(2026-10-30): 클라이언트 주소는 `X-Forwarded-For` 의 마지막 칸(외부 프록시가 이어 붙인 값 — 없으면 접속 주소). 워커마다 `worker_connections` 4096 · 열린 파일 8192.
|
||||
받기 location 은 `/api/` 블록 안의 `~*` 정규식(id · 토큰 칸 `.+`) — 600개를 붙잡아 401번째 공개 · 201번째 앱 안이 429 이고 그때도 다른 쪽 · SSE 는 200 임을 확인했다.
|
||||
받기 머리글 버퍼 16k(한글 255자 이름이면 3.5KB). 공개 server 의 그 밖 `/api` · 포털이 닫은 공개 API(대소문자 변형도)는 nginx 의 404 `RES_001` JSON, 1m 넘는 본문은 413 `FILE_002` JSON.
|
||||
조각 경로는 오류 로그를 `error` 부터 남긴다(본문을 임시 파일에 받는다는 warn 이 조각마다 남았다).
|
||||
백엔드는 종료할 때 진행 중인 파일 응답을 먼저 끊는다(`BeforeApplicationShutdown` — 실시간 알림과 같은 결, 브라우저는 Range 로 잇는다).
|
||||
백엔드의 공개 호스트 판정은 nginx 가 넘기는 `Host`(`$host` — 소문자 · 포트 · 끝 점을 뗀 값)와 origin-form 요청 줄에 기댄다 — files 호스트의 server 도 `proxy_set_header Host $host` 를 둔다.
|
||||
- 외부 리버스 프록시: `files.` 호스트(같은 사설 주소:8080 · TLS), 조각 경로 본문 17MB 이상 · 읽기 150초, 내려받기 응답의 버퍼링 · 임시 파일 끔(또는 `X-Accel-Buffering` 따르기), 공개 경로는 **경로까지** 접근 로그에 남기지 않기, 받기 응답 머리글 버퍼 16k(NPM 은 `proxy_buffer_size` · `proxy_busy_buffers_size` 두 줄).
|
||||
- 볼륨 `<PROJECT_NAME>_files` — 크기 · 디스크(DB · 엔진과 다른 디스크 검토) · 감시, 백업에서 빼기, `down -v` 가 지운다는 경고. Dockerfile 이 `/app/files` 를 node 소유로 만든다.
|
||||
- ClamAV 컨테이너(위 "악성 검사" — clamd 는 띄울 때 `/tmp/clamav-*` 를 지운다: 검사 중에 죽으면 임시 파일이 쓰기 계층에 쌓였다). 사내 도메인을 꾸민 바깥 메일(From)은 DMARC 로 막는다(읽기 화면의 "보낸 사람 = 링크 주인" 판정이 기댄다 — 아래 "알려진 한계").
|
||||
- 확인(2026-10-30 — 운영 nginx 설정): 버퍼링을 끈 내려받기에서 `proxy_read_timeout` 은 돌지 않는다. 받는 쪽이 느려도 조금씩 받으면 이어지고(초당 120KB 로 80초),
|
||||
60초 동안 하나도 받지 않으면 `send_timeout`(기본 60초)이 끊고 동시 연결 수를 돌려준다.
|
||||
- 남은 확인(시험): 브라우저(Chrome · Edge · Firefox · Safari)가 우리 머리글로 끊긴 내려받기를 이어 받는지.
|
||||
|
||||
### 알려진 한계
|
||||
|
||||
- 같은 사본이라 받는 사람별 링크 · 기록 · 끊기가 없다 — 받은 수는 "링크를 가진 누군가"의 수다.
|
||||
- 횟수는 0번째 바이트부터 받기 시작한 수다 — 범위 요청만으로 받으면 세지 않는다(열린 질문 1).
|
||||
거꾸로 토큰을 가진 사람이 `Range: bytes=0-0` 같은 작은 요청으로 상한을 빨리 써 버리면 같은 링크의 다른 받는 사람이 410 이 된다(IP 하나로 몇 분) — 닫힌 쪽이라 새는 것은 없고, 보낸 사람이 다시 보내면 된다(2026-10-30 리뷰 — 보낸 바이트 기준 상한은 처음 판에 넣지 않는다).
|
||||
- 끊어도 이미 시작한 내려받기는 끝까지 간다. 받아 간 파일은 되돌릴 수 없다.
|
||||
- 인용 속 원래 링크는 글이다 — 우리 웹메일도 첨부처럼 보이지 않고, 원래 링크의 받은 수 · 상한 · 기간을 따른다(전달받은 사람이 받아도 원래 보낸 사람의 받은 수로 센다).
|
||||
- 읽기 화면의 "보낸 사람 = 링크 주인" 판정은 머리글 From 을 믿는다 — 사내 도메인을 꾸민 메일이 그 직원의 진짜 토큰을 담으면 첨부처럼 보인다(파일은 진짜다. DMARC 가 막는다 — 운영 확인).
|
||||
- 다른 메일 프로그램이 우리 임시 저장본을 그대로 보내면 머리글만 나가고 덩이는 없다. 우리 API 밖에서 지운 임시 저장본의 파일은 기간까지 보관량을 잡는다(보내지 않은 파일 목록에서 지울 수 있다).
|
||||
- 텍스트만 있는 메일 · HTML 이 정화 한도를 넘은 메일의 덩이는 알아보지 않는다.
|
||||
- `SHARE_PUBLIC_URL` 을 바꾸면 그 전 메일의 덩이를 알아보지 않는다.
|
||||
- 경로 토큰이 nginx 오류 로그 · 외부 프록시 로그에 남을 수 있다.
|
||||
- 검사는 한 번이다 — 나중에 나온 서명으로 다시 보지 않는다.
|
||||
- ClamAV 1.4.6 은 zip **안** 항목이 크기 한도(`MaxFileSize` · `MaxScanSize`)를 넘을 때 `AlertExceedsMax` 를 켜도 `OK` 로 줄 때가 있다(한도를 작게 둔 시험 clamd 에서 확인 —
|
||||
운영 값에서는 `MaxScanTime` 이 먼저 걸렸다). 7z · tar.gz 처럼 zip 이 아닌 압축은 시간 상한 없이 `MaxScanSize` 까지 풀어 검사가 오래 걸릴 수 있다.
|
||||
- 내려받기 동시 연결 상한은 `X-Forwarded-For` 의 마지막 칸으로 센다 — 외부 프록시를 건너뛰어 8080 에 바로 닿는 길이 있으면 주소마다 상한을 피한다(경로마다 서버 전체 상한은 그대로).
|
||||
IPv6 는 주소 그대로 센다(/64 로 묶지 않는다).
|
||||
- 한 인스턴스 전제 — 올리기의 동시 수 · 만들기 차례 · 같은 조각 잠금은 서버 메모리, 파일은 로컬 볼륨이다. 인스턴스를 늘리면 공유 저장(S3 호환 — 같은 저장 포트의 새 어댑터)과 잠금으로 옮긴다
|
||||
(nestjs 스킬의 "확장 때 옮길 것"에 더한다).
|
||||
|
||||
### 이 단계에서 하지 않는 것
|
||||
|
||||
기간 고르기 · 연장, 다시 보내기(다시 올리지 않고 새 링크), 받는 사람 확인 코드 · 비밀번호, 사내만(로그인해야 받기), 받는 사람별 링크 · 기록, 처음 받음 · 만료 전 알림, 미리 보기, 모두 받기(zip), 폴더 올리기,
|
||||
파일 하나만 끊기(끊기는 링크 단위), 설정 목록의 [링크 복사](토큰을 되살릴 수 없다 — 보낸 메일의 덩이에서), 대용량 첨부만 있는 메일의 목록 클립, 링크 → 일반 첨부로 되돌리기,
|
||||
전달 원본 · 임시 저장본의 엔진 첨부를 링크로 바꾸기, 인용 속 원래 링크를 다시 싣기,
|
||||
관리자 화면 · 퇴사자 처리(신고를 받은 링크는 그때까지 운영 스크립트로 끊는다 — 구현하며 정함), 보낸 메일을 지울 때 파일도 지우기, 모바일 앱, 파일 저장소 연동,
|
||||
다른 PC · 다른 브라우저에서 이어 올리기(같은 브라우저에서 같은 파일을 다시 고르면 이어진다).
|
||||
|
||||
### 정한 것 (열린 질문이었던 것 — 2026-10-30 추천대로)
|
||||
|
||||
1. **횟수 상한의 범위 요청 우회** — 0번째 바이트를 받지 않는 범위 요청은 세지 않아, 도구를 쓰면 상한을 피할 수 있다. 추천: 처음 판은 그대로(브라우저 · 내려받기 관리자는 0번부터 받는다)이고 한계로 적는다.
|
||||
남용이 보이면 "보낸 바이트 ÷ 파일 크기" 상한을 함께 둔다.
|
||||
2. **위험 판정 알림** — 추천: 실시간 이벤트 + 목록 상태(처음 판). 연결이 없던 직원까지 알리는 받은편지함 시스템 메일은 털린 계정을 알아채는 데 좋지만 엔진에 메일을 넣는 길이 새로 든다 — 13단계(관리자 절차)와 함께 다시 본다.
|
||||
3. **검사기 장애가 길 때** — 추천: 받기를 막은 채(검사 중) 두고 error 로그로 운영이 고친다(닫힌 쪽). 대안 — 몇 시간 뒤 "검사하지 못함"으로 연다(받는 쪽이 오래 기다리지 않지만 검사 없이 퍼진다).
|
||||
4. **ClamAV 크기 한도** — 기본 2,047MB 가 운영에 쓸 clamd 판에서 끝까지 검사되는지 실측한다(자료는 "2GB 넘는 파일은 검사하지 못함" — 판마다 다를 수 있다). 안 되면 기본값을 낮춘다.
|
||||
실측(2026-10-30, ClamAV 1.4.6): 2,047MB 는 끝까지 검사된다(`OK`) — 기본값 그대로 둔다. 1.4.6 은 2GiB − 1 바이트 넘게는 검사하지 못한다.
|
||||
5. **위험 파일의 바이트** — 추천: 곧 지운다(디스크에 악성 파일을 두지 않고 보관량에서 바로 뺀다). 오탐을 다시 보려면 서명 갱신 뒤 다시 올린다.
|
||||
6. **일반 첨부 목록 밖 · 대용량 목록 안 형식의 작은 파일**(5MB 짜리 mp4 · dwg 등) — 지금 일반 첨부로는 보낼 수 없는 형식이다. 추천: 크기와 상관없이 링크로(쓰기 화면이 자동으로 — 이 쓰기 화면에서 고른 파일만이라 위 "손으로 「링크로 보내기」" 규칙과 맞는다).
|
||||
7. **보내지 않은 파일 목록에 올리는 중인 것도 넣을지** — 추천: 넣는다(이 계약). 보관량을 잡고 있어, 창을 닫아 멈춘 올리기를 24시간 전에 지울 길이 된다.
|
||||
8. **공개 호스트의 화면 묶음** — 추천: 처음 판은 같은 SPA 묶음 + nginx 가 `/s/` · 자산 · `/api/public-links/` 만 연다. 공개 화면에 앱 코드를 싣지 않으려면 Vite 다중 입구(`share.html`)를 쓴다.
|
||||
9. **`FILE_006` 의 HTTP 상태** — 추천: 507(저장 공간을 정확히 말하고, 클라이언트의 자동 다시 시도 대상(502 · 503 · 504)과 상태로 갈린다). 대안 503.
|
||||
10. **형식 검사의 MIME** — 추천: 확장자 허용 목록만(위 "올리기" 1). 보안 규칙(`.claude/rules/security.md` "확장자와 MIME 을 둘 다")의 예외라 규칙 문서에 까닭과 함께 적는다(rules 담당).
|
||||
11. **인용 속 링크 상태를 묻는 길** — 추천: 주소 목록을 보내는 `POST /api/share-link-lookups`(이 계약 — 저장하지 않은 쓰기 화면의 인용에도 쓴다). 대안 — 원본 메일 id 로 서버가 본문에서 찾기(`GET /api/messages/{id}/…`) — 클라이언트가 주소를 고르지 않아도 되지만 임시 저장본 · 고친 인용은 따로 다뤄야 한다.
|
||||
|
||||
Reference in New Issue
Block a user