mirror of
https://github.com/15000rpm/Pole-Mapper.git
synced 2026-10-09 10:45:10 +09:00
테스트 서버에 올리기 위한 준비
ID 중복 방지 - UUID v4 사용
This commit is contained in:
@@ -83,6 +83,21 @@ export async function insertPole(pole) {
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export async function findDuplicatePole(lat, lng, timestamp) {
|
||||
const tolerance = 0.0001;
|
||||
const { data, error } = await supabase
|
||||
.from('poles')
|
||||
.select('id')
|
||||
.gte('lat', lat - tolerance)
|
||||
.lte('lat', lat + tolerance)
|
||||
.gte('lng', lng - tolerance)
|
||||
.lte('lng', lng + tolerance)
|
||||
.eq('timestamp', timestamp)
|
||||
.limit(1);
|
||||
if (error) throw error;
|
||||
return data?.length > 0;
|
||||
}
|
||||
|
||||
export async function getPole(id) {
|
||||
const { data, error } = await supabase
|
||||
.from('poles')
|
||||
|
||||
+45
-9
@@ -4,13 +4,19 @@ import cors from 'cors';
|
||||
import multer from 'multer';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as db from './db.js';
|
||||
import { fetchGus, fetchDongs } from './vworld.js';
|
||||
import { fetchGus, fetchDongs, reverseGeocode } from './vworld.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const UPLOAD_DIR = path.join(__dirname, 'uploads');
|
||||
const LEVEL_DIRS = ['A', 'B', 'Pole'];
|
||||
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
for (const dir of LEVEL_DIRS) {
|
||||
fs.mkdirSync(path.join(UPLOAD_DIR, dir), { recursive: true });
|
||||
}
|
||||
|
||||
const PORT = Number(process.env.PORT || 4000);
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
@@ -22,7 +28,12 @@ app.use('/uploads', express.static(UPLOAD_DIR));
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (_req, _file, cb) => cb(null, UPLOAD_DIR),
|
||||
destination: (req, _file, cb) => {
|
||||
const level = req.body?.level || 'uncategorized';
|
||||
const dir = path.join(UPLOAD_DIR, level);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
cb(null, dir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const id = req.body?.id || `pole-${Date.now()}`;
|
||||
const ext = path.extname(file.originalname) || '.jpg';
|
||||
@@ -70,19 +81,40 @@ app.get('/api/poles', async (req, res) => {
|
||||
});
|
||||
|
||||
app.post('/api/poles', upload.single('photo'), async (req, res) => {
|
||||
const { id, lat, lng, gu = '', dong = '', timestamp, level = '' } = req.body ?? {};
|
||||
const { id, lat, lng, timestamp, level = '' } = req.body ?? {};
|
||||
let { gu = '', dong = '' } = req.body ?? {};
|
||||
if (lat === undefined || lng === undefined || !timestamp) {
|
||||
return res.status(400).json({ error: 'lat, lng, timestamp는 필수입니다.' });
|
||||
}
|
||||
const numLat = Number(lat);
|
||||
const numLng = Number(lng);
|
||||
try {
|
||||
if (await db.findDuplicatePole(numLat, numLng, timestamp)) {
|
||||
if (req.file) fs.rm(req.file.path, () => {});
|
||||
return res.status(409).json({ error: 'duplicate', message: '이미 등록된 전신주입니다.' });
|
||||
}
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: e.message });
|
||||
}
|
||||
if (!gu || !dong) {
|
||||
try {
|
||||
const geo = await reverseGeocode(numLat, numLng);
|
||||
if (!gu) gu = geo.gu;
|
||||
if (!dong) dong = geo.dong;
|
||||
} catch (e) {
|
||||
console.warn('지오코딩 실패:', e.message);
|
||||
}
|
||||
}
|
||||
const photoPath = req.file ? `${level || 'uncategorized'}/${req.file.filename}` : null;
|
||||
const pole = {
|
||||
id: id || `pole-${Date.now()}`,
|
||||
lat: Number(lat),
|
||||
lng: Number(lng),
|
||||
id: id || `pole-${crypto.randomUUID()}`,
|
||||
lat: numLat,
|
||||
lng: numLng,
|
||||
gu,
|
||||
dong,
|
||||
timestamp,
|
||||
level,
|
||||
photo_path: req.file ? req.file.filename : null,
|
||||
photo_path: photoPath,
|
||||
};
|
||||
try {
|
||||
await db.insertPole(pole);
|
||||
@@ -118,7 +150,7 @@ app.delete('/api/poles/:id', async (req, res) => {
|
||||
const existing = await db.getPole(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: '전신주를 찾을 수 없습니다.' });
|
||||
if (existing.photo_path) {
|
||||
fs.rm(path.join(UPLOAD_DIR, path.basename(existing.photo_path)), () => {});
|
||||
fs.rm(path.join(UPLOAD_DIR, existing.photo_path), () => {});
|
||||
}
|
||||
try {
|
||||
await db.deletePole(existing.id);
|
||||
@@ -128,6 +160,10 @@ app.delete('/api/poles/:id', async (req, res) => {
|
||||
res.json({ removed: true });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`Pole Mapper API server: http://localhost:${PORT}`);
|
||||
});
|
||||
server.on('error', (err) => {
|
||||
console.error('서버 시작 실패:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dotenv/config';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
import { join, basename, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SOURCE_URL = 'https://saftyovhcable.kr/addMarker.html';
|
||||
const THUMB_BASE = 'https://saftyovhcable.kr/res/Thumb/';
|
||||
const UPLOAD_DIR = join(import.meta.dirname, 'uploads');
|
||||
const UPLOAD_DIR = join(__dirname, 'uploads');
|
||||
|
||||
const supabaseUrl = process.env.SUPABASE_URL;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
@@ -63,3 +63,48 @@ export async function fetchDongs(guCode) {
|
||||
const features = await fetchVworldFeatures('LT_C_ADEMD_INFO', `emd_cd:like:${guCode}`);
|
||||
return features.map((f) => f.properties.emd_kor_nm).filter(Boolean);
|
||||
}
|
||||
|
||||
export async function reverseGeocode(lat, lng) {
|
||||
const { key, domain } = credentials();
|
||||
if (!key) throw new Error('VWORLD_API_KEY 환경변수가 설정되지 않았습니다.');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
service: 'address',
|
||||
request: 'getaddress',
|
||||
version: '2.0',
|
||||
crs: 'epsg:4326',
|
||||
point: `${lng},${lat}`,
|
||||
format: 'json',
|
||||
type: 'PARCEL',
|
||||
key,
|
||||
domain,
|
||||
});
|
||||
|
||||
const res = await fetch(`${VWORLD_ENDPOINT}?${params}`);
|
||||
if (!res.ok) throw new Error(`VWorld 지오코딩 실패 (HTTP ${res.status})`);
|
||||
const json = await res.json();
|
||||
const result = json.response?.result;
|
||||
|
||||
if (!result?.text) return { gu: '', dong: '' };
|
||||
|
||||
const text = result.text;
|
||||
const structure = result.structure;
|
||||
|
||||
let gu = '';
|
||||
let dong = '';
|
||||
|
||||
if (structure) {
|
||||
gu = structure.level1 || '';
|
||||
dong = structure.level2 || '';
|
||||
} else {
|
||||
const parts = text.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
gu = parts[0];
|
||||
dong = parts[1];
|
||||
} else if (parts.length === 1) {
|
||||
gu = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
return { gu, dong };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user