Initial commit: MapinDrag - 근무 배치 시스템
Flask + Leaflet 기반 지도 드래그앤드롭 근무 배치 시스템 - 엑셀 업로드/파싱 - Leaflet 지도에 근무지/근무자 표시 - 드래그앤드롭으로 근무자 배치 - 엑셀 내보내기
This commit is contained in:
207
app.py
Normal file
207
app.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import os
|
||||
import io
|
||||
import random
|
||||
import requests
|
||||
import 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 = {
|
||||
'workers': [],
|
||||
'workplaces': [],
|
||||
'assignments': {},
|
||||
'filename': 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
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@app.route('/upload', methods=['POST'])
|
||||
def upload():
|
||||
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()
|
||||
hope = str(row[idx.get('희망사항', 1)] or '').strip()
|
||||
addr = str(row[idx.get('주소', 2)] or '').strip()
|
||||
dob = str(row[idx.get('생년월일', 3)] or '').strip()
|
||||
phone = str(row[idx.get('연락처', 4)] or '').strip()
|
||||
if not name:
|
||||
continue
|
||||
lat, lng = geocode_with_fallback(addr)
|
||||
workers.append({
|
||||
'id': f'w{len(workers)}',
|
||||
'name': name,
|
||||
'hope': hope,
|
||||
'address': addr,
|
||||
'dob': dob,
|
||||
'phone': phone,
|
||||
'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()
|
||||
addr = str(row[idx.get('주소', 1)] or '').strip()
|
||||
if not name:
|
||||
continue
|
||||
lat, lng = geocode_with_fallback(addr)
|
||||
workplaces.append({
|
||||
'id': f'p{len(workplaces)}',
|
||||
'name': name,
|
||||
'address': addr,
|
||||
'lat': lat,
|
||||
'lng': lng,
|
||||
})
|
||||
|
||||
store['workers'] = workers
|
||||
store['workplaces'] = workplaces
|
||||
store['assignments'] = {}
|
||||
store['filename'] = file.filename
|
||||
|
||||
return jsonify({
|
||||
'workers': workers,
|
||||
'workplaces': workplaces,
|
||||
'assignments': {},
|
||||
})
|
||||
|
||||
|
||||
@app.route('/data')
|
||||
def get_data():
|
||||
return jsonify({
|
||||
'workers': store['workers'],
|
||||
'workplaces': store['workplaces'],
|
||||
'assignments': store['assignments'],
|
||||
})
|
||||
|
||||
|
||||
@app.route('/assign', methods=['POST'])
|
||||
def assign():
|
||||
body = request.get_json()
|
||||
worker_id = body.get('workerId')
|
||||
workplace_id = body.get('workplaceId')
|
||||
if not worker_id or not workplace_id:
|
||||
return jsonify({'error': '잘못된 요청입니다.'}), 400
|
||||
store['assignments'][worker_id] = workplace_id
|
||||
return jsonify({'assignments': store['assignments']})
|
||||
|
||||
|
||||
@app.route('/unassign', methods=['POST'])
|
||||
def unassign():
|
||||
body = request.get_json()
|
||||
worker_id = body.get('workerId')
|
||||
store['assignments'].pop(worker_id, None)
|
||||
return jsonify({'assignments': store['assignments']})
|
||||
|
||||
|
||||
@app.route('/reset', methods=['POST'])
|
||||
def reset():
|
||||
store['assignments'] = {}
|
||||
return jsonify({'assignments': {}})
|
||||
|
||||
|
||||
@app.route('/export')
|
||||
def export():
|
||||
wb = openpyxl.Workbook()
|
||||
ws1 = wb.active
|
||||
ws1.title = '배치 현황'
|
||||
ws1.append(['이름', '연락처', '생년월일', '희망사항', '주소', '배치근무지', '근무지주소'])
|
||||
|
||||
wp_lookup = {wp['id']: wp for wp in store['workplaces']}
|
||||
for w in store['workers']:
|
||||
assigned = store['assignments'].get(w['id'])
|
||||
wp_name = ''
|
||||
wp_addr = ''
|
||||
if assigned and assigned in wp_lookup:
|
||||
wp_name = wp_lookup[assigned]['name']
|
||||
wp_addr = wp_lookup[assigned]['address']
|
||||
ws1.append([w['name'], w['phone'], w['dob'], w['hope'], w['address'], wp_name, wp_addr])
|
||||
|
||||
ws2 = wb.create_sheet('근무지별 배치')
|
||||
ws2.append(['근무지명', '주소', '배치인원', '배치된근무자'])
|
||||
|
||||
for wp in store['workplaces']:
|
||||
assigned_workers = [
|
||||
w['name'] for w in store['workers']
|
||||
if store['assignments'].get(w['id']) == wp['id']
|
||||
]
|
||||
count = len(assigned_workers)
|
||||
ws2.append([wp['name'], wp['address'], count, ', '.join(assigned_workers)])
|
||||
|
||||
buf = io.BytesIO()
|
||||
wb.save(buf)
|
||||
buf.seek(0)
|
||||
|
||||
name = store.get('filename', 'export.xlsx')
|
||||
base, ext = os.path.splitext(name)
|
||||
download_name = f'{base}_배치결과{ext}'
|
||||
|
||||
return send_file(
|
||||
buf,
|
||||
as_attachment=True,
|
||||
download_name=download_name,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
Reference in New Issue
Block a user