import os, io, random, requests, openpyxl from flask import Flask, render_template, request, jsonify, send_file app = Flask(__name__) app.config['UPLOAD_FOLDER'] = 'uploads' app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) YONGSAN_CENTER = (37.5326, 126.9906) store = { 'tabs': {}, 'active_tab': None, 'next_id': 0, 'filename': None, } def tab_data(tab_id): t = store['tabs'].get(tab_id) if not t: return None, 404 return t, None def geocode(address): try: url = 'https://nominatim.openstreetmap.org/search' params = {'q': address.strip(), 'format': 'json', 'limit': 1} headers = {'User-Agent': 'MapinDrag/1.0'} resp = requests.get(url, params=params, headers=headers, timeout=10) if resp.ok and resp.json(): r = resp.json()[0] return float(r['lat']), float(r['lon']) except Exception: pass return None, None def geocode_with_fallback(address): lat, lng = geocode(f'{address}, 대한민국') if lat is not None: return lat, lng lat, lng = geocode(f'서울특별시 용산구 {address}') if lat is not None: return lat, lng lat, lng = geocode(f'용산구 {address}') if lat is not None: return lat, lng jitter = random.uniform(-0.005, 0.005) return YONGSAN_CENTER[0] + jitter, YONGSAN_CENTER[1] + jitter def empty_tab(name): return {'name': name, 'workers': [], 'workplaces': [], 'assignments': {}} # ── Tab management ── @app.route('/api/tabs') def list_tabs(): tabs = [{'id': tid, 'name': t['name'], 'workerCount': len(t['workers']), 'assignCount': len(t['assignments'])} for tid, t in store['tabs'].items()] return jsonify({'tabs': tabs, 'activeTab': store['active_tab']}) @app.route('/api/tabs/add', methods=['POST']) def add_tab(): body = request.get_json() or {} name = (body.get('name') or '').strip() or f'탭 {store["next_id"] + 1}' tid = f'tab_{store["next_id"]}' store['next_id'] += 1 store['tabs'][tid] = empty_tab(name) if store['active_tab'] is None: store['active_tab'] = tid return jsonify({'id': tid, 'name': name}) @app.route('/api/tabs/rename', methods=['POST']) def rename_tab(): body = request.get_json() or {} tid = body.get('id') name = (body.get('name') or '').strip() if not tid or not name or tid not in store['tabs']: return jsonify({'error': '잘못된 요청'}), 400 store['tabs'][tid]['name'] = name return jsonify({'id': tid, 'name': name}) @app.route('/api/tabs/delete', methods=['POST']) def delete_tab(): body = request.get_json() or {} tid = body.get('id') if not tid or tid not in store['tabs']: return jsonify({'error': '잘못된 요청'}), 400 del store['tabs'][tid] if store['active_tab'] == tid: keys = list(store['tabs'].keys()) store['active_tab'] = keys[0] if keys else None return jsonify({'deleted': tid}) @app.route('/api/tabs/activate', methods=['POST']) def activate_tab(): body = request.get_json() or {} tid = body.get('id') if not tid or tid not in store['tabs']: return jsonify({'error': '탭을 찾을 수 없음'}), 400 store['active_tab'] = tid return jsonify({'activeTab': tid}) # ── Data ── def get_active(): t = store['tabs'].get(store['active_tab']) if not t: return None, jsonify({'error': '탭 없음'}), 400 return t, None, None @app.route('/api/data') def get_data(): tid = request.args.get('tab') or store['active_tab'] t = store['tabs'].get(tid) if not t: return jsonify({'workers': [], 'workplaces': [], 'assignments': {}}) return jsonify({ 'workers': t['workers'], 'workplaces': t['workplaces'], 'assignments': t['assignments'], }) @app.route('/api/upload', methods=['POST']) def upload(): tid = request.form.get('tab') or store['active_tab'] t = store['tabs'].get(tid) if not t: return jsonify({'error': '탭 없음'}), 400 file = request.files.get('file') if not file: return jsonify({'error': '파일이 없습니다.'}), 400 path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename) file.save(path) wb = openpyxl.load_workbook(path, data_only=True) workers, workplaces = [], [] for sheet_name in wb.sheetnames: ws = wb[sheet_name] headers = [str(c.value or '').strip() for c in ws[1]] if '희망사항' in headers: idx = {h: i for i, h in enumerate(headers)} for row in ws.iter_rows(min_row=2, values_only=True): if not any(row): continue name = str(row[idx.get('이름', 0)] or '').strip() if not name: continue lat, lng = geocode_with_fallback(str(row[idx.get('주소', 2)] or '').strip()) workers.append({ 'id': f'w{len(workers)}', 'name': name, 'hope': str(row[idx.get('희망사항', 1)] or '').strip(), 'address': str(row[idx.get('주소', 2)] or '').strip(), 'dob': str(row[idx.get('생년월일', 3)] or '').strip(), 'phone': str(row[idx.get('연락처', 4)] or '').strip(), 'lat': lat, 'lng': lng, }) if '희망사항' not in headers: idx = {h: i for i, h in enumerate(headers)} for row in ws.iter_rows(min_row=2, values_only=True): if not any(row): continue name = str(row[idx.get('이름', 0)] or '').strip() if not name: continue lat, lng = geocode_with_fallback(str(row[idx.get('주소', 1)] or '').strip()) workplaces.append({ 'id': f'p{len(workplaces)}', 'name': name, 'address': str(row[idx.get('주소', 1)] or '').strip(), 'lat': lat, 'lng': lng, }) t['workers'] = workers t['workplaces'] = workplaces t['assignments'] = {} store['filename'] = file.filename return jsonify({'workers': workers, 'workplaces': workplaces, 'assignments': {}}) @app.route('/api/assign', methods=['POST']) def assign(): body = request.get_json() or {} tid = body.get('tab') or store['active_tab'] t = store['tabs'].get(tid) if not t: return jsonify({'error': '탭 없음'}), 400 worker_id, workplace_id = body.get('workerId'), body.get('workplaceId') if not worker_id or not workplace_id: return jsonify({'error': '잘못된 요청'}), 400 t['assignments'][worker_id] = workplace_id return jsonify({'assignments': t['assignments']}) @app.route('/api/unassign', methods=['POST']) def unassign(): body = request.get_json() or {} tid = body.get('tab') or store['active_tab'] t = store['tabs'].get(tid) if not t: return jsonify({'error': '탭 없음'}), 400 worker_id = body.get('workerId') t['assignments'].pop(worker_id, None) return jsonify({'assignments': t['assignments']}) @app.route('/api/reset', methods=['POST']) def reset(): body = request.get_json() or {} tid = body.get('tab') or store['active_tab'] t = store['tabs'].get(tid) if t: t['assignments'] = {} return jsonify({'assignments': t['assignments'] if t else {}}) @app.route('/api/export') def export(): tid = request.args.get('tab') or store['active_tab'] t = store['tabs'].get(tid) if not t: return jsonify({'error': '탭 없음'}), 400 wb = openpyxl.Workbook() ws1 = wb.active ws1.title = '배치 현황' ws1.append(['이름', '연락처', '생년월일', '희망사항', '주소', '배치근무지', '근무지주소']) wp_lookup = {wp['id']: wp for wp in t['workplaces']} for w in t['workers']: a_id = t['assignments'].get(w['id']) wn = wp_lookup[a_id]['name'] if a_id and a_id in wp_lookup else '' wa = wp_lookup[a_id]['address'] if a_id and a_id in wp_lookup else '' ws1.append([w['name'], w['phone'], w['dob'], w['hope'], w['address'], wn, wa]) ws2 = wb.create_sheet('근무지별 배치') ws2.append(['근무지명', '주소', '배치인원', '배치된근무자']) for wp in t['workplaces']: aw = [w['name'] for w in t['workers'] if t['assignments'].get(w['id']) == wp['id']] ws2.append([wp['name'], wp['address'], len(aw), ', '.join(aw)]) buf = io.BytesIO() wb.save(buf) buf.seek(0) tab_name = t['name'] fname = store.get('filename', 'export.xlsx') base, ext = os.path.splitext(fname) dn = f'{base}_{tab_name}_배치결과{ext}' return send_file(buf, as_attachment=True, download_name=dn, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run(debug=True)