HAPPY SOLUTION · 실무도구

건물 관리비 정산도구

건물 기본정보와 호실을 설정하고, 이번 달 검침값 또는 금액만 입력하면 호실별 관리비 정산서를 깔끔하게 만들 수 있습니다.

1. 건물 기본정보

입금계좌와 기본 안내문은 세입자 전달용 정산서에 표시됩니다.

2. 호실 등록

3. 관리비 항목과 계산방식

건물에서 실제 사용하는 항목만 추가하세요. 요금 단가는 직접 확인한 값을 입력합니다.

4. 이번 달 검침값 / 금액

호실을 선택하면 이번 달 수치를 입력할 수 있습니다.

다음 달에도 그대로 이어서

지난달 작성본을 열어 복제하거나 다음 달 정산을 만드세요. 건물정보·호실·항목·고정비는 유지합니다.

다음 달: 당월 검침 → 전월 검침, 새 검침·전체 분배금·직접입력은 비웁니다. 납부기한은 다음 달 날짜를 제안하므로 확인해주세요.

건물주 내부 메모 · 정산서에서 제외

계산 기준: 금액은 원 단위입니다. 사용량 × 단가의 원 미만은 반올림합니다. 공용비 분배의 잔여원은 사용중 호실의 등록순으로 1원씩 더해 총액을 맞춥니다. 미사용 호실은 계산에서 제외합니다.

입력 내용은 서버로 전송하지 않습니다. 최종 보관은 작성본 파일 저장을 기준으로 합니다.

호실별 저장 파일에는 해당 호실 정산서만 들어갑니다. 다른 호실과 내부 메모는 포함하지 않습니다.

작성본 HTML은 기기에 저장한 뒤 브라우저로 다시 열어 이어서 작성할 수 있습니다. 다운로드 후에는 인터넷 없이 사용할 수 있습니다. 일부 메신저 미리보기에서는 기능이 동작하지 않을 수 있습니다. PDF는 인쇄 화면의 ‘PDF로 저장’을 선택해주세요.
직접 입력한 요금과 분담방식을 정리하는 기록도구입니다. 건물별 계약·약정과 실제 고지금액을 확인해주세요. 요금 단가·세금·미납 가산금을 자동 산정하지 않습니다.

';} function printRooms(all=false){const rooms=all?active():[current()];if(!ready(rooms))return;printingIds=rooms.map(r=>r.id);renderInvoices(rooms);hsToolAction('pdf');window.print();printingIds=null;if(document.body.classList.contains('preview'))renderPreview();} function shiftMonth(value){if(!MONTH.test(value))return null;let y=Number(value.slice(0,4)),m=Number(value.slice(5));if(m===12){y++;m=1;}else m++;if(y>9999)return null;return y.toString().padStart(4,'0')+'-'+String(m).padStart(2,'0');} function shiftDue(value){if(!/^\d{4}-\d{2}-\d{2}$/.test(value))return '';const next=shiftMonth(value.slice(0,7));if(!next)return '';const y=Number(next.slice(0,4)),m=Number(next.slice(5)),last=new Date(Date.UTC(y,m,0)).getUTCDate();return next+'-'+String(Math.min(Number(value.slice(8)),last)).padStart(2,'0');} function monthCopy(next){if(!state.rooms.length&&!state.fees.length){status('먼저 건물의 호실과 항목을 설정해주세요.');return;}const month=next?shiftMonth(state.basic.month):state.basic.month;if(next&&!month){status('정산 월을 먼저 확인해주세요.');return;}if(!confirm('현재 작성본은 먼저 파일로 저장해주세요. '+(next?'다음 달 화면으로 전환하며 변동 수치는 초기화합니다.':'현재 자료를 새 작성본으로 복제합니다. 정산 월과 수치를 확인해주세요.')+' 계속할까요?'))return;const s=normalize(JSON.parse(JSON.stringify(state)));s.id=uid();s.savedAt=0;if(next){s.basic.month=month;s.basic.due=shiftDue(s.basic.due);s.fees.forEach(f=>{if(f.method==='shared')f.value='';s.rooms.forEach(r=>{const e=values(r,f);if(f.method==='usage'){const parsed=number(e.curr,true);e.prev=parsed.error?'':e.curr;e.curr='';}if(f.method==='manual')e.manual='';});});}state=s;selected=s.rooms[0]?.id||'';document.body.classList.remove('preview');renderBuilding();renderRooms();renderFees();renderEntries();changed();status(next?'다음 달 정산을 만들었습니다. 새 검침값·변동금액·납부기한과 내부 메모를 확인해주세요.':'지난달 자료를 복제했습니다. 정산 월과 이번 달 수치를 확인해주세요.');} $('addRooms').addEventListener('click',()=>{const names=$('roomNames').value.split(/[\n,]/).map(n=>n.trim()).filter(Boolean);if(!names.length){status('추가할 호실명을 입력해주세요.');return;}const distinct=[...new Set(names)].filter(name=>!state.rooms.some(r=>r.name.trim()===name));if(state.rooms.length+distinct.length>100||distinct.some(n=>n.length>80)){status('호실명은 80자까지, 호실은 100개까지 지원합니다.');return;}distinct.forEach(n=>state.rooms.push(room(n)));selected=state.rooms.at(-1)?.id||selected;$('roomNames').value='';changed();renderRooms();renderEntries();status(distinct.length+'호실을 추가했습니다. 같은 호실명은 중복 등록하지 않습니다.');}); $('addFee').addEventListener('click',()=>{const name=$('feeName').value.trim();if(!name){status('관리비 항목명을 입력해주세요.');return;}if(state.fees.length>=40){status('관리비 항목은 40개까지 지원합니다.');return;}const f=fee(name,$('feeMethod').value);state.fees.push(f);state.rooms.forEach(r=>{r.values[f.id]=entry();});$('feeName').value='';changed();renderFees(f.id);renderEntries();status('항목을 추가했습니다. 요금과 계산방식을 확인해주세요.');}); $('internalNote').addEventListener('input',()=>{state.internal=$('internalNote').value;changed();});$('viewInvoice').addEventListener('click',()=>setPreview(true));$('backToEditor').addEventListener('click',()=>setPreview(false));$('saveOwner').addEventListener('click',()=>{download(ownerHTML(),filePart(state.basic.building)+'_관리비_작성본_'+filePart(state.basic.month)+'.html');dirty=false;status('전체 작성본을 저장했습니다. 세입자에게는 호실별 정산서 파일만 전달해주세요.');});$('saveRoom').addEventListener('click',()=>{const r=current();if(!ready([r]))return;download(publicHTML(r),filePart(state.basic.building)+'_'+filePart(r.name)+'_관리비_'+filePart(state.basic.month)+'.html');status('선택 호실 정산서를 저장했습니다. 내부 메모와 다른 호실은 파일에 포함하지 않습니다.');});$('printRoom').addEventListener('click',()=>printRooms());$('printPreview').addEventListener('click',()=>printRooms());$('printAll').addEventListener('click',()=>printRooms(true));$('printAllPreview').addEventListener('click',()=>printRooms(true));$('duplicateMonth').addEventListener('click',()=>monthCopy(false));$('nextMonth').addEventListener('click',()=>monthCopy(true)); $('autosave').addEventListener('change',()=>{if($('autosave').checked)changed();else{try{localStorage.removeItem(STORAGE+state.id);$('storageHelp').textContent='임시 자동저장을 끄고 현재 작성본의 임시저장을 제거했습니다. 작성본 파일로 보관해주세요.';}catch{$('storageHelp').textContent='임시 자동저장을 껐습니다. 공유 기기에서는 브라우저 저장 데이터도 확인해주세요.';}}}); try{const raw=localStorage.getItem(STORAGE+state.id);if(raw){candidateDraft=normalize(JSON.parse(raw));if(candidateDraft&&candidateDraft.savedAt>state.savedAt)$('restoreNotice').hidden=false;}}catch{} $('restoreDraft').addEventListener('click',()=>{if(!candidateDraft)return;if(dirty&&!confirm('현재 입력 대신 임시 작성본을 불러올까요? 현재 작성본은 먼저 저장해주세요.'))return;state=candidateDraft;selected=state.rooms[0]?.id||'';candidateDraft=null;$('restoreNotice').hidden=true;renderBuilding();renderRooms();renderFees();renderEntries();dirty=false;status('임시 작성본을 불러왔습니다.');}); window.addEventListener('beforeprint',()=>{const rooms=printingIds?state.rooms.filter(r=>printingIds.includes(r.id)):[current()];if(rooms.every(Boolean)&&rooms.length&&rooms.every(r=>errorsFor(r).length===0))renderInvoices(rooms);else $('invoices').replaceChildren(el('p','입력 확인이 필요한 정산서입니다. 작성 화면에서 검침값과 금액을 확인해주세요.','warning'));});window.addEventListener('beforeunload',e=>{if(dirty){e.preventDefault();e.returnValue='';}}); renderBuilding();renderRooms();renderFees();renderEntries(); })();