Add: Contact 문의 폼 + Discord 봇 연동 + PIPA 개인정보처리방침
- 문의 폼 프론트엔드 (이름, 이메일, 전화, 문의유형, 메시지, 개인정보동의) - api-server.py에 /api/contact 엔드포인트 추가 (Discord Bot API 2채널) - bot/discord_cleanup.py: 7일 후 자동 삭제 봇 - 개인정보처리방침 모달 (PIPA 10개 항목, 국외이전 명시) - 민감정보 입력 경고 문구 - 개인정보 동의 체크 시에만 제출 버튼 활성화 - API 실패 시 대체 연락 안내 오버레이 (이메일/전화/Discord) - 4개 언어 i18n 지원 (ko/en/zh/ja) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
baf19fe0b4
commit
8384d1a1d9
20
.env.example
Normal file
20
.env.example
Normal file
@ -0,0 +1,20 @@
|
||||
# ─── Discord 봇 설정 ─────────────────────────────
|
||||
# 봇 토큰: https://discord.com/developers/applications 에서 봇 생성 후 토큰 복사
|
||||
DISCORD_BOT_TOKEN=YOUR_BOT_TOKEN_HERE
|
||||
|
||||
# 문의 상세 내용이 전달될 채널 ID (본계정만 열람 가능한 비공개 채널)
|
||||
DISCORD_INQUIRY_CHANNEL_ID=YOUR_INQUIRY_CHANNEL_ID
|
||||
|
||||
# 관리자 알림 채널 ID (멘션으로 알림만 보냄)
|
||||
DISCORD_NOTIFY_CHANNEL_ID=YOUR_NOTIFY_CHANNEL_ID
|
||||
|
||||
# 관리자 Discord User ID (쉼표 구분)
|
||||
# Discord 개발자 모드 켜기 > 유저 우클릭 > ID 복사
|
||||
DISCORD_ADMIN_IDS=123456789,987654321,111222333
|
||||
|
||||
# ─── 자동 삭제 설정 ──────────────────────────────
|
||||
# 문의 채널 메시지 삭제까지 보관 일수 (기본: 7일)
|
||||
DELETE_AFTER_DAYS=7
|
||||
|
||||
# 삭제 체크 주기 (시간 단위, 기본: 6시간)
|
||||
CHECK_INTERVAL_HOURS=6
|
||||
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
145
api-server.py
145
api-server.py
@ -10,6 +10,8 @@ import socketserver
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
import ssl
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@ -31,6 +33,54 @@ HOST = '127.0.0.1' # 로컬만 허용 (Caddy가 프록시)
|
||||
WORKING_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
os.chdir(WORKING_DIR)
|
||||
|
||||
# Discord 설정 (.env 파일에서 로드)
|
||||
DISCORD_BOT_TOKEN = ''
|
||||
DISCORD_INQUIRY_CHANNEL_ID = '' # 문의 상세 (본계정만 열람)
|
||||
DISCORD_NOTIFY_CHANNEL_ID = '' # 관리자 알림 (멘션)
|
||||
DISCORD_ADMIN_IDS = [] # 멘션할 관리자 User ID 목록
|
||||
|
||||
def load_env():
|
||||
"""Load .env file"""
|
||||
global DISCORD_BOT_TOKEN, DISCORD_INQUIRY_CHANNEL_ID, DISCORD_NOTIFY_CHANNEL_ID, DISCORD_ADMIN_IDS
|
||||
env_path = os.path.join(WORKING_DIR, '.env')
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if key == 'DISCORD_BOT_TOKEN':
|
||||
DISCORD_BOT_TOKEN = value
|
||||
elif key == 'DISCORD_INQUIRY_CHANNEL_ID':
|
||||
DISCORD_INQUIRY_CHANNEL_ID = value
|
||||
elif key == 'DISCORD_NOTIFY_CHANNEL_ID':
|
||||
DISCORD_NOTIFY_CHANNEL_ID = value
|
||||
elif key == 'DISCORD_ADMIN_IDS':
|
||||
DISCORD_ADMIN_IDS = [id.strip() for id in value.split(',') if id.strip()]
|
||||
|
||||
load_env()
|
||||
|
||||
def discord_bot_send(channel_id, payload):
|
||||
"""Discord Bot API로 메시지 전송"""
|
||||
url = f'https://discord.com/api/v10/channels/{channel_id}/messages'
|
||||
body = json.dumps(payload).encode('utf-8')
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bot {DISCORD_BOT_TOKEN}'
|
||||
},
|
||||
method='POST'
|
||||
)
|
||||
ctx = ssl.create_default_context()
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=10) as resp:
|
||||
if resp.status not in (200, 201):
|
||||
raise Exception(f'Discord API failed: {resp.status}')
|
||||
return json.loads(resp.read().decode('utf-8'))
|
||||
|
||||
class APIRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
"""API 전용 HTTP 요청 핸들러"""
|
||||
|
||||
@ -67,6 +117,8 @@ class APIRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.handle_post_backgrounds()
|
||||
elif parsed_path.path == '/api/props':
|
||||
self.handle_post_props()
|
||||
elif parsed_path.path == '/api/contact':
|
||||
self.handle_post_contact()
|
||||
else:
|
||||
self.send_error_json(404, 'API endpoint not found')
|
||||
|
||||
@ -200,6 +252,91 @@ class APIRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
except Exception as e:
|
||||
self.send_error_json(500, f'데이터 저장 실패: {str(e)}')
|
||||
|
||||
def handle_post_contact(self):
|
||||
"""문의 폼 처리 → Discord Bot API (2채널: 문의상세 + 관리자알림)"""
|
||||
try:
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
if content_length == 0:
|
||||
self.send_error_json(400, '요청 본문이 비어있습니다')
|
||||
return
|
||||
|
||||
post_data = self.rfile.read(content_length)
|
||||
try:
|
||||
data = json.loads(post_data.decode('utf-8'))
|
||||
except json.JSONDecodeError as e:
|
||||
self.send_error_json(400, f'잘못된 JSON 형식: {str(e)}')
|
||||
return
|
||||
|
||||
# 필수 필드 검증
|
||||
required = ['name', 'email', 'service', 'message']
|
||||
for field in required:
|
||||
if not data.get(field, '').strip():
|
||||
self.send_error_json(400, f'{field} 필드가 필요합니다')
|
||||
return
|
||||
|
||||
# 문의 유형 한글 매핑
|
||||
service_map = {
|
||||
'studio_rental': '스튜디오 대관',
|
||||
'vtuber': 'VTuber 제작',
|
||||
'mocap': '모션캡쳐 촬영',
|
||||
'music_video': '뮤직비디오 제작',
|
||||
'partnership': '제휴/협력',
|
||||
'other': '기타'
|
||||
}
|
||||
service_label = service_map.get(data['service'], data['service'])
|
||||
|
||||
# Discord 봇 설정 확인
|
||||
if not DISCORD_BOT_TOKEN or not DISCORD_INQUIRY_CHANNEL_ID:
|
||||
print("[API] Warning: Discord bot not configured")
|
||||
self.send_error_json(500, '서버 설정 오류')
|
||||
return
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
# 1) 문의 상세 채널에 전체 내용 전송
|
||||
embed = {
|
||||
'title': '📩 새로운 문의가 접수되었습니다',
|
||||
'color': 0xff8800,
|
||||
'fields': [
|
||||
{'name': '👤 이름', 'value': data['name'], 'inline': True},
|
||||
{'name': '📧 이메일', 'value': data['email'], 'inline': True},
|
||||
{'name': '📱 전화번호', 'value': data.get('phone', '-') or '-', 'inline': True},
|
||||
{'name': '📋 문의 유형', 'value': service_label, 'inline': True},
|
||||
{'name': '💬 문의 내용', 'value': data['message'][:1024], 'inline': False},
|
||||
],
|
||||
'footer': {'text': f'⏰ {now.strftime("%Y-%m-%d %H:%M")} | 7일 후 자동 삭제'},
|
||||
'timestamp': now.isoformat()
|
||||
}
|
||||
discord_bot_send(DISCORD_INQUIRY_CHANNEL_ID, {'embeds': [embed]})
|
||||
|
||||
# 2) 관리자 알림 채널에 멘션 알림 전송
|
||||
if DISCORD_NOTIFY_CHANNEL_ID and DISCORD_ADMIN_IDS:
|
||||
mentions = ' '.join(f'<@{uid}>' for uid in DISCORD_ADMIN_IDS)
|
||||
notify_msg = f'📩 **고객 문의가 접수되었습니다!**\n\n> **{data["name"]}**님 | {service_label}\n\n{mentions}'
|
||||
discord_bot_send(DISCORD_NOTIFY_CHANNEL_ID, {
|
||||
'content': notify_msg,
|
||||
'allowed_mentions': {'users': DISCORD_ADMIN_IDS}
|
||||
})
|
||||
|
||||
# 성공 응답
|
||||
response = {
|
||||
'success': True,
|
||||
'message': '문의가 성공적으로 전송되었습니다.'
|
||||
}
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(response, ensure_ascii=False).encode('utf-8'))
|
||||
|
||||
print(f"[API] 문의 접수: {data['name']} ({service_label})")
|
||||
|
||||
except urllib.error.URLError as e:
|
||||
print(f"[API] Discord API error: {e}")
|
||||
self.send_error_json(500, '문의 전송에 실패했습니다. 잠시 후 다시 시도해 주세요.')
|
||||
except Exception as e:
|
||||
print(f"[API] Contact error: {e}")
|
||||
self.send_error_json(500, f'문의 처리 실패: {str(e)}')
|
||||
|
||||
def send_error_json(self, code, message):
|
||||
"""JSON 형식 에러 응답"""
|
||||
self.send_response(code)
|
||||
@ -228,6 +365,14 @@ def main():
|
||||
print(f" POST /api/backgrounds")
|
||||
print(f" GET /api/props")
|
||||
print(f" POST /api/props")
|
||||
print(f" POST /api/contact → Discord Bot API")
|
||||
if DISCORD_BOT_TOKEN:
|
||||
print(f" Discord Bot: configured")
|
||||
print(f" Inquiry channel: {DISCORD_INQUIRY_CHANNEL_ID or 'NOT SET'}")
|
||||
print(f" Notify channel: {DISCORD_NOTIFY_CHANNEL_ID or 'NOT SET'}")
|
||||
print(f" Admin mentions: {len(DISCORD_ADMIN_IDS)} user(s)")
|
||||
else:
|
||||
print(f" ⚠ Discord Bot: NOT configured (set DISCORD_BOT_TOKEN in .env)")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
|
||||
195
bot/discord_cleanup.py
Normal file
195
bot/discord_cleanup.py
Normal file
@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
밍글 스튜디오 Discord 문의 자동 삭제 봇
|
||||
- 지정된 채널의 메시지를 7일 후 자동 삭제
|
||||
- 개인정보보호법(PIPA) 준수를 위한 데이터 보관기간 관리
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
import ssl
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
# ─── 설정 ───────────────────────────────────────
|
||||
BOT_TOKEN = ''
|
||||
CHANNEL_ID = ''
|
||||
DELETE_AFTER_DAYS = 7
|
||||
CHECK_INTERVAL_HOURS = 6 # 삭제 체크 주기 (시간)
|
||||
|
||||
def load_env():
|
||||
"""Load .env file from parent directory"""
|
||||
global BOT_TOKEN, CHANNEL_ID, DELETE_AFTER_DAYS, CHECK_INTERVAL_HOURS
|
||||
|
||||
env_paths = [
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '.env'),
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env'),
|
||||
]
|
||||
|
||||
for env_path in env_paths:
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if key == 'DISCORD_BOT_TOKEN':
|
||||
BOT_TOKEN = value
|
||||
elif key == 'DISCORD_INQUIRY_CHANNEL_ID':
|
||||
CHANNEL_ID = value
|
||||
elif key == 'DELETE_AFTER_DAYS':
|
||||
DELETE_AFTER_DAYS = int(value)
|
||||
elif key == 'CHECK_INTERVAL_HOURS':
|
||||
CHECK_INTERVAL_HOURS = int(value)
|
||||
break
|
||||
|
||||
def discord_api(method, endpoint, data=None):
|
||||
"""Discord API 호출"""
|
||||
url = f'https://discord.com/api/v10{endpoint}'
|
||||
headers = {
|
||||
'Authorization': f'Bot {BOT_TOKEN}',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
body = json.dumps(data).encode('utf-8') if data else None
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=15) as resp:
|
||||
if resp.status == 204:
|
||||
return None
|
||||
return json.loads(resp.read().decode('utf-8'))
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode('utf-8') if e.fp else ''
|
||||
print(f"[ERROR] Discord API {method} {endpoint}: {e.code} {error_body}")
|
||||
|
||||
# Rate limit 처리
|
||||
if e.code == 429:
|
||||
retry_data = json.loads(error_body) if error_body else {}
|
||||
retry_after = retry_data.get('retry_after', 5)
|
||||
print(f"[RATE LIMIT] Waiting {retry_after}s...")
|
||||
time.sleep(retry_after + 0.5)
|
||||
return discord_api(method, endpoint, data)
|
||||
|
||||
raise
|
||||
|
||||
def get_old_messages():
|
||||
"""지정 채널에서 만료된 메시지 조회"""
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=DELETE_AFTER_DAYS)
|
||||
old_messages = []
|
||||
last_id = None
|
||||
|
||||
while True:
|
||||
params = f'?limit=100'
|
||||
if last_id:
|
||||
params += f'&before={last_id}'
|
||||
|
||||
messages = discord_api('GET', f'/channels/{CHANNEL_ID}/messages{params}')
|
||||
|
||||
if not messages:
|
||||
break
|
||||
|
||||
for msg in messages:
|
||||
# Discord 타임스탬프 파싱
|
||||
msg_time = datetime.fromisoformat(msg['timestamp'].replace('+00:00', '+00:00'))
|
||||
if msg_time.tzinfo is None:
|
||||
msg_time = msg_time.replace(tzinfo=timezone.utc)
|
||||
|
||||
if msg_time < cutoff:
|
||||
old_messages.append({
|
||||
'id': msg['id'],
|
||||
'timestamp': msg['timestamp'],
|
||||
'content': msg.get('content', '')[:50]
|
||||
})
|
||||
|
||||
last_id = messages[-1]['id']
|
||||
|
||||
# 가장 오래된 메시지도 만료 기간 내라면 더 조회할 필요 없음
|
||||
oldest_time = datetime.fromisoformat(messages[-1]['timestamp'].replace('+00:00', '+00:00'))
|
||||
if oldest_time.tzinfo is None:
|
||||
oldest_time = oldest_time.replace(tzinfo=timezone.utc)
|
||||
if oldest_time >= cutoff:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
time.sleep(0.5) # Rate limit 방지
|
||||
|
||||
return old_messages
|
||||
|
||||
def delete_messages(messages):
|
||||
"""메시지 삭제"""
|
||||
deleted = 0
|
||||
for msg in messages:
|
||||
try:
|
||||
discord_api('DELETE', f'/channels/{CHANNEL_ID}/messages/{msg["id"]}')
|
||||
print(f" [DELETE] {msg['id']} ({msg['timestamp'][:10]})")
|
||||
deleted += 1
|
||||
time.sleep(0.5) # Rate limit 방지
|
||||
except Exception as e:
|
||||
print(f" [ERROR] Failed to delete {msg['id']}: {e}")
|
||||
return deleted
|
||||
|
||||
def run_cleanup():
|
||||
"""한 번의 정리 사이클 실행"""
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f"\n[{now}] Cleanup check started (delete after {DELETE_AFTER_DAYS} days)")
|
||||
|
||||
try:
|
||||
messages = get_old_messages()
|
||||
|
||||
if messages:
|
||||
print(f" Found {len(messages)} expired message(s)")
|
||||
deleted = delete_messages(messages)
|
||||
print(f" Deleted {deleted}/{len(messages)} message(s)")
|
||||
else:
|
||||
print(f" No expired messages found")
|
||||
|
||||
except Exception as e:
|
||||
print(f" [ERROR] Cleanup failed: {e}")
|
||||
|
||||
def main():
|
||||
"""메인 실행"""
|
||||
load_env()
|
||||
|
||||
if not BOT_TOKEN:
|
||||
print("Error: DISCORD_BOT_TOKEN not set in .env")
|
||||
sys.exit(1)
|
||||
if not CHANNEL_ID:
|
||||
print("Error: DISCORD_CHANNEL_ID not set in .env")
|
||||
sys.exit(1)
|
||||
|
||||
print("Mingle Studio Discord Cleanup Bot")
|
||||
print("=" * 40)
|
||||
print(f"Channel ID: {CHANNEL_ID}")
|
||||
print(f"Delete after: {DELETE_AFTER_DAYS} days")
|
||||
print(f"Check interval: {CHECK_INTERVAL_HOURS} hours")
|
||||
print("=" * 40)
|
||||
|
||||
# 봇 정보 확인
|
||||
try:
|
||||
bot_info = discord_api('GET', '/users/@me')
|
||||
print(f"Bot: {bot_info['username']}#{bot_info.get('discriminator', '0')}")
|
||||
except Exception as e:
|
||||
print(f"Error: Failed to connect to Discord: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print("Bot started. Press Ctrl+C to stop.\n")
|
||||
|
||||
try:
|
||||
while True:
|
||||
run_cleanup()
|
||||
print(f" Next check in {CHECK_INTERVAL_HOURS} hours...")
|
||||
time.sleep(CHECK_INTERVAL_HOURS * 3600)
|
||||
except KeyboardInterrupt:
|
||||
print("\nBot stopped.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
179
contact.html
179
contact.html
@ -190,6 +190,185 @@
|
||||
</section>
|
||||
|
||||
|
||||
<!-- 온라인 문의 폼 -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 data-i18n="contact.form.title">온라인 문의</h2>
|
||||
<p data-i18n="contact.form.desc">아래 양식을 작성하시면 빠르게 답변 드리겠습니다</p>
|
||||
</div>
|
||||
|
||||
<div class="contact-form-wrapper">
|
||||
<form id="contactForm" class="contact-form" novalidate>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="name" data-i18n="contact.form.name">이름 <span class="required">*</span></label>
|
||||
<input type="text" id="name" name="name" required placeholder="홍길동" data-i18n-placeholder="contact.form.namePlaceholder">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email" data-i18n="contact.form.email">이메일 <span class="required">*</span></label>
|
||||
<input type="email" id="email" name="email" required placeholder="example@email.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="phone" data-i18n="contact.form.phone">전화번호</label>
|
||||
<input type="tel" id="phone" name="phone" placeholder="010-0000-0000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service" data-i18n="contact.form.service">문의 유형 <span class="required">*</span></label>
|
||||
<select id="service" name="service" required>
|
||||
<option value="" data-i18n="contact.form.serviceDefault">선택해주세요</option>
|
||||
<option value="studio_rental" data-i18n="contact.form.serviceRental">스튜디오 대관</option>
|
||||
<option value="vtuber" data-i18n="contact.form.serviceVtuber">VTuber 제작</option>
|
||||
<option value="mocap" data-i18n="contact.form.serviceMocap">모션캡쳐 촬영</option>
|
||||
<option value="music_video" data-i18n="contact.form.serviceMV">뮤직비디오 제작</option>
|
||||
<option value="partnership" data-i18n="contact.form.servicePartner">제휴/협력</option>
|
||||
<option value="other" data-i18n="contact.form.serviceOther">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message" data-i18n="contact.form.message">문의 내용 <span class="required">*</span></label>
|
||||
<textarea id="message" name="message" required rows="5" placeholder="프로젝트 내용, 희망 일정 등을 자유롭게 작성해주세요" data-i18n-placeholder="contact.form.messagePlaceholder"></textarea>
|
||||
<p class="field-hint" data-i18n="contact.form.sensitiveWarning">※ 주민등록번호, 계좌번호 등 민감한 개인정보를 입력하지 마세요.</p>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="privacyConsent" name="privacyConsent" required>
|
||||
<span class="checkmark"></span>
|
||||
<span data-i18n="contact.form.privacyAgree">개인정보 수집 및 이용에 동의합니다. (필수)</span>
|
||||
</label>
|
||||
<a href="javascript:void(0)" class="privacy-link" onclick="openPrivacyModal()" data-i18n="contact.form.privacyView">개인정보 처리방침 보기</a>
|
||||
</div>
|
||||
|
||||
<div class="privacy-summary">
|
||||
<ul>
|
||||
<li data-i18n="contact.form.privacyPurpose">수집 목적: 문의 접수 및 답변</li>
|
||||
<li data-i18n="contact.form.privacyItems">수집 항목: 이름, 이메일, 전화번호, 문의 내용</li>
|
||||
<li data-i18n="contact.form.privacyPeriod">보유 기간: 7일 후 자동 파기</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-submit">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary btn-lg btn-submit-disabled" disabled data-i18n="contact.form.submit">문의 보내기</button>
|
||||
<button type="reset" class="btn btn-outline btn-lg" data-i18n="contact.form.reset">초기화</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 개인정보처리방침 모달 -->
|
||||
<div id="privacyModal" class="modal" onclick="if(event.target===this)closePrivacyModal()" role="dialog" aria-modal="true" aria-labelledby="privacyModalTitle">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="privacyModalTitle">개인정보 수집 및 이용 동의서</h3>
|
||||
<button class="modal-close" onclick="closePrivacyModal()" aria-label="닫기">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>밍글 스튜디오(이하 "회사")는 「개인정보 보호법」에 따라 고객의 개인정보를 보호하고 이와 관련한 고충을 신속하게 처리하기 위하여 다음과 같이 개인정보 수집 및 이용에 대한 동의를 구합니다.</p>
|
||||
|
||||
<h4>1. 개인정보의 수집 및 이용 목적</h4>
|
||||
<p>회사는 다음의 목적을 위하여 개인정보를 처리합니다.</p>
|
||||
<ul>
|
||||
<li>고객 문의 접수 및 답변 제공</li>
|
||||
<li>서비스 상담 및 견적 안내</li>
|
||||
<li>문의 처리 결과 통보</li>
|
||||
</ul>
|
||||
|
||||
<h4>2. 수집하는 개인정보의 항목</h4>
|
||||
<ul>
|
||||
<li><strong>필수 항목:</strong> 이름, 이메일 주소, 문의 유형, 문의 내용</li>
|
||||
<li><strong>선택 항목:</strong> 전화번호</li>
|
||||
</ul>
|
||||
<p style="color: #ef4444; font-size: 0.85rem;">※ 문의 내용에 주민등록번호, 계좌번호 등 민감한 개인정보가 포함되지 않도록 유의해 주시기 바랍니다.</p>
|
||||
|
||||
<h4>3. 개인정보의 보유 및 이용 기간</h4>
|
||||
<p>수집된 개인정보는 <strong>문의 접수일로부터 7일간</strong> 보유하며, 보유 기간 경과 후 자동으로 파기됩니다. 다만, 관계 법령에 의한 보존 의무가 있는 경우 해당 법령에서 정한 기간 동안 보존합니다.</p>
|
||||
|
||||
<h4>4. 개인정보의 파기 절차 및 방법</h4>
|
||||
<ul>
|
||||
<li><strong>파기 절차:</strong> 보유 기간이 경과한 개인정보는 자동 삭제 시스템에 의해 지체 없이 파기됩니다.</li>
|
||||
<li><strong>파기 방법:</strong> 전자적 파일 형태의 정보는 복구 및 재생할 수 없도록 완전 삭제합니다.</li>
|
||||
</ul>
|
||||
|
||||
<h4>5. 개인정보의 제3자 제공 및 국외 이전</h4>
|
||||
<p>회사는 원칙적으로 이용자의 개인정보를 제3자에게 제공하지 않습니다. 다만, 문의 처리를 위해 다음과 같이 개인정보가 국외로 이전됩니다. (「개인정보 보호법」 제28조의8)</p>
|
||||
<ul>
|
||||
<li><strong>이전되는 국가 및 수신자:</strong> 미국 / Discord Inc.</li>
|
||||
<li><strong>이전 일시 및 방법:</strong> 문의 접수 시 전용 API(인터넷 망)를 통해 실시간 전송</li>
|
||||
<li><strong>이전 항목:</strong> 이름, 이메일, 전화번호(선택), 문의 유형, 문의 내용</li>
|
||||
<li><strong>이전 목적:</strong> 업무용 협업 툴(Discord)을 이용한 신속한 문의 확인 및 응대</li>
|
||||
<li><strong>보유 및 이용 기간:</strong> 전송 후 7일간 보관 후 자동 파기</li>
|
||||
</ul>
|
||||
|
||||
<h4>6. 정보주체의 권리·의무</h4>
|
||||
<p>이용자는 개인정보 주체로서 다음과 같은 권리를 행사할 수 있습니다.</p>
|
||||
<ul>
|
||||
<li>개인정보 열람 요구</li>
|
||||
<li>오류 등이 있을 경우 정정 요구</li>
|
||||
<li>삭제 요구</li>
|
||||
<li>처리정지 요구</li>
|
||||
</ul>
|
||||
<p>위 권리 행사는 아래 개인정보 보호책임자에게 연락하여 하실 수 있으며, 회사는 이에 대해 지체 없이 조치하겠습니다.</p>
|
||||
|
||||
<h4>7. 개인정보의 안전성 확보 조치</h4>
|
||||
<p>회사는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.</p>
|
||||
<ul>
|
||||
<li>전송 데이터 암호화 (HTTPS/TLS)</li>
|
||||
<li>접근 권한 제한 (지정된 관리자만 열람 가능)</li>
|
||||
<li>자동 삭제 시스템 운영 (7일 후 파기)</li>
|
||||
</ul>
|
||||
|
||||
<h4>8. 동의 거부 권리 및 불이익</h4>
|
||||
<p>이용자는 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다. 다만, 필수 항목에 대한 동의를 거부하실 경우 문의 접수 서비스 이용이 제한됩니다.</p>
|
||||
|
||||
<h4>9. 개인정보 보호책임자</h4>
|
||||
<ul>
|
||||
<li><strong>성명:</strong> 김희진</li>
|
||||
<li><strong>직위:</strong> 대표</li>
|
||||
<li><strong>이메일:</strong> minglestudio@minglestudio.co.kr</li>
|
||||
<li><strong>전화:</strong> 010-9288-9190</li>
|
||||
</ul>
|
||||
|
||||
<h4>10. 개인정보처리방침의 변경</h4>
|
||||
<p>이 개인정보처리방침은 2026년 3월 4일부터 적용됩니다. 변경 사항이 있을 경우 웹사이트를 통해 공지합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// 모달 열기/닫기 (인라인 — 외부 JS 캐시 무관하게 즉시 동작)
|
||||
function openPrivacyModal() {
|
||||
var m = document.getElementById('privacyModal');
|
||||
if (!m) return;
|
||||
m.classList.add('active');
|
||||
m.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
function closePrivacyModal() {
|
||||
var m = document.getElementById('privacyModal');
|
||||
if (!m) return;
|
||||
m.classList.remove('active');
|
||||
m.style.display = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
// 체크박스 → 제출 버튼 활성화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var cb = document.getElementById('privacyConsent');
|
||||
var btn = document.getElementById('submitBtn');
|
||||
if (cb && btn) {
|
||||
cb.addEventListener('change', function() {
|
||||
btn.disabled = !cb.checked;
|
||||
btn.classList.toggle('btn-submit-disabled', !cb.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 위치 정보 -->
|
||||
<section id="location" class="section">
|
||||
<div class="container">
|
||||
|
||||
202
css/contact.css
202
css/contact.css
@ -203,6 +203,186 @@
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* 필드 힌트 */
|
||||
.field-hint {
|
||||
color: #ef4444;
|
||||
font-size: var(--font-xs);
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 비활성 제출 버튼 */
|
||||
.btn-submit-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* API 에러 폴백 오버레이 */
|
||||
.api-error-fallback {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.api-error-content {
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--border-radius);
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
padding: var(--spacing-2xl);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.api-error-close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.api-error-close:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.api-error-icon {
|
||||
font-size: 3rem;
|
||||
color: #ef4444;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.api-error-content h3 {
|
||||
margin: 0 0 var(--spacing-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.api-error-content > p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-sm);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.api-error-contacts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.api-error-method {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.api-error-method:hover {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(255, 136, 0, 0.05);
|
||||
}
|
||||
|
||||
.api-error-method i {
|
||||
font-size: 1.25rem;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.api-error-method div {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.api-error-method strong {
|
||||
display: block;
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.api-error-method span {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 다크모드 API 에러 */
|
||||
[data-theme="dark"] .api-error-content {
|
||||
background: var(--dark-bg-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .api-error-content h3 {
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .api-error-content > p {
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .api-error-method {
|
||||
border-color: var(--dark-border);
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .api-error-method:hover {
|
||||
background: rgba(255, 136, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .api-error-method span {
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
/* 개인정보 요약 */
|
||||
.privacy-summary {
|
||||
background: var(--bg-light, #f8f9fa);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-secondary);
|
||||
border-left: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.privacy-summary ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.privacy-summary li {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.privacy-summary li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 필수 표시 */
|
||||
.required {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .privacy-summary {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-left-color: var(--primary-color);
|
||||
color: var(--dark-text-tertiary);
|
||||
}
|
||||
|
||||
/* 폼 제출 버튼 */
|
||||
.form-submit {
|
||||
display: flex;
|
||||
@ -353,6 +533,25 @@
|
||||
max-height: 80vh;
|
||||
width: 90%;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--primary-color) transparent;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--secondary-color);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@ -372,6 +571,9 @@
|
||||
font-size: var(--font-2xl);
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
|
||||
178
en/contact.html
178
en/contact.html
@ -197,6 +197,184 @@
|
||||
</section>
|
||||
|
||||
|
||||
<!-- 온라인 문의 폼 -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 data-i18n="contact.form.title">온라인 문의</h2>
|
||||
<p data-i18n="contact.form.desc">아래 양식을 작성하시면 빠르게 답변 드리겠습니다</p>
|
||||
</div>
|
||||
|
||||
<div class="contact-form-wrapper">
|
||||
<form id="contactForm" class="contact-form" novalidate>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="name" data-i18n="contact.form.name">이름 <span class="required">*</span></label>
|
||||
<input type="text" id="name" name="name" required placeholder="홍길동" data-i18n-placeholder="contact.form.namePlaceholder">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email" data-i18n="contact.form.email">이메일 <span class="required">*</span></label>
|
||||
<input type="email" id="email" name="email" required placeholder="example@email.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="phone" data-i18n="contact.form.phone">전화번호</label>
|
||||
<input type="tel" id="phone" name="phone" placeholder="010-0000-0000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service" data-i18n="contact.form.service">문의 유형 <span class="required">*</span></label>
|
||||
<select id="service" name="service" required>
|
||||
<option value="" data-i18n="contact.form.serviceDefault">선택해주세요</option>
|
||||
<option value="studio_rental" data-i18n="contact.form.serviceRental">스튜디오 대관</option>
|
||||
<option value="vtuber" data-i18n="contact.form.serviceVtuber">VTuber 제작</option>
|
||||
<option value="mocap" data-i18n="contact.form.serviceMocap">모션캡쳐 촬영</option>
|
||||
<option value="music_video" data-i18n="contact.form.serviceMV">뮤직비디오 제작</option>
|
||||
<option value="partnership" data-i18n="contact.form.servicePartner">제휴/협력</option>
|
||||
<option value="other" data-i18n="contact.form.serviceOther">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message" data-i18n="contact.form.message">문의 내용 <span class="required">*</span></label>
|
||||
<textarea id="message" name="message" required rows="5" placeholder="프로젝트 내용, 희망 일정 등을 자유롭게 작성해주세요" data-i18n-placeholder="contact.form.messagePlaceholder"></textarea>
|
||||
<p class="field-hint" data-i18n="contact.form.sensitiveWarning">※ 주민등록번호, 계좌번호 등 민감한 개인정보를 입력하지 마세요.</p>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="privacyConsent" name="privacyConsent" required>
|
||||
<span class="checkmark"></span>
|
||||
<span data-i18n="contact.form.privacyAgree">개인정보 수집 및 이용에 동의합니다. (필수)</span>
|
||||
</label>
|
||||
<a href="javascript:void(0)" class="privacy-link" onclick="openPrivacyModal()" data-i18n="contact.form.privacyView">개인정보 처리방침 보기</a>
|
||||
</div>
|
||||
|
||||
<div class="privacy-summary">
|
||||
<ul>
|
||||
<li data-i18n="contact.form.privacyPurpose">수집 목적: 문의 접수 및 답변</li>
|
||||
<li data-i18n="contact.form.privacyItems">수집 항목: 이름, 이메일, 전화번호, 문의 내용</li>
|
||||
<li data-i18n="contact.form.privacyPeriod">보유 기간: 7일 후 자동 파기</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-submit">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary btn-lg btn-submit-disabled" disabled data-i18n="contact.form.submit">문의 보내기</button>
|
||||
<button type="reset" class="btn btn-outline btn-lg" data-i18n="contact.form.reset">초기화</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 개인정보처리방침 모달 -->
|
||||
<div id="privacyModal" class="modal" onclick="if(event.target===this)closePrivacyModal()" role="dialog" aria-modal="true" aria-labelledby="privacyModalTitle">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="privacyModalTitle">Consent to Collection and Use of Personal Information</h3>
|
||||
<button class="modal-close" onclick="closePrivacyModal()" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Mingle Studio (hereinafter "the Company") seeks your consent for the collection and use of personal information in accordance with the Personal Information Protection Act (PIPA) of South Korea.</p>
|
||||
|
||||
<h4>1. Purpose of Collection and Use</h4>
|
||||
<p>The Company processes personal information for the following purposes:</p>
|
||||
<ul>
|
||||
<li>Receiving and responding to customer inquiries</li>
|
||||
<li>Service consultation and quotation guidance</li>
|
||||
<li>Notification of inquiry processing results</li>
|
||||
</ul>
|
||||
|
||||
<h4>2. Items of Personal Information Collected</h4>
|
||||
<ul>
|
||||
<li><strong>Required:</strong> Name, email address, inquiry type, message</li>
|
||||
<li><strong>Optional:</strong> Phone number</li>
|
||||
</ul>
|
||||
<p style="color: #ef4444; font-size: 0.85rem;">※ Please do not include sensitive personal information such as national ID numbers or bank account numbers in your inquiry.</p>
|
||||
|
||||
<h4>3. Retention and Use Period</h4>
|
||||
<p>Collected personal information is retained for <strong>7 days from the date of inquiry</strong> and is automatically destroyed after the retention period. However, if there is an obligation to preserve under relevant laws, it will be preserved for the period prescribed by such laws.</p>
|
||||
|
||||
<h4>4. Destruction Procedure and Method</h4>
|
||||
<ul>
|
||||
<li><strong>Procedure:</strong> Personal information that has exceeded the retention period is destroyed without delay by an automated deletion system.</li>
|
||||
<li><strong>Method:</strong> Electronic file information is completely deleted so that it cannot be recovered or reproduced.</li>
|
||||
</ul>
|
||||
|
||||
<h4>5. Provision to Third Parties and Overseas Transfer</h4>
|
||||
<p>The Company does not provide personal information to third parties in principle. However, for inquiry processing, personal information is transferred overseas as follows (pursuant to Article 28-8 of PIPA):</p>
|
||||
<ul>
|
||||
<li><strong>Country and recipient:</strong> United States / Discord Inc.</li>
|
||||
<li><strong>Timing and method:</strong> Real-time transmission via dedicated API (internet network) upon inquiry submission</li>
|
||||
<li><strong>Items transferred:</strong> Name, email, phone number (optional), inquiry type, message</li>
|
||||
<li><strong>Purpose:</strong> Prompt inquiry confirmation and response via collaboration tool (Discord)</li>
|
||||
<li><strong>Retention and use period:</strong> Stored for 7 days after transmission, then automatically destroyed</li>
|
||||
</ul>
|
||||
|
||||
<h4>6. Rights of Data Subjects</h4>
|
||||
<p>Users may exercise the following rights as data subjects:</p>
|
||||
<ul>
|
||||
<li>Request to access personal information</li>
|
||||
<li>Request correction of errors</li>
|
||||
<li>Request deletion</li>
|
||||
<li>Request suspension of processing</li>
|
||||
</ul>
|
||||
<p>These rights can be exercised by contacting the Personal Information Protection Officer below, and the Company will take action without delay.</p>
|
||||
|
||||
<h4>7. Safety Measures</h4>
|
||||
<p>The Company takes the following measures to ensure the safety of personal information:</p>
|
||||
<ul>
|
||||
<li>Data transmission encryption (HTTPS/TLS)</li>
|
||||
<li>Access restriction (only designated administrators can view)</li>
|
||||
<li>Automated deletion system (destroyed after 7 days)</li>
|
||||
</ul>
|
||||
|
||||
<h4>8. Right to Refuse Consent</h4>
|
||||
<p>Users have the right to refuse consent to the collection and use of personal information. However, refusal of consent for required items will restrict the use of the inquiry service.</p>
|
||||
|
||||
<h4>9. Personal Information Protection Officer</h4>
|
||||
<ul>
|
||||
<li><strong>Name:</strong> Kim Heejin</li>
|
||||
<li><strong>Position:</strong> CEO</li>
|
||||
<li><strong>Email:</strong> minglestudio@minglestudio.co.kr</li>
|
||||
<li><strong>Phone:</strong> +82-10-9288-9190</li>
|
||||
</ul>
|
||||
|
||||
<h4>10. Changes to This Policy</h4>
|
||||
<p>This privacy policy is effective from March 4, 2026. Any changes will be announced through this website.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function openPrivacyModal() {
|
||||
var m = document.getElementById('privacyModal');
|
||||
if (!m) return;
|
||||
m.classList.add('active');
|
||||
m.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
function closePrivacyModal() {
|
||||
var m = document.getElementById('privacyModal');
|
||||
if (!m) return;
|
||||
m.classList.remove('active');
|
||||
m.style.display = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
// 체크박스 → 제출 버튼 활성화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var cb = document.getElementById('privacyConsent');
|
||||
var btn = document.getElementById('submitBtn');
|
||||
if (cb && btn) {
|
||||
cb.addEventListener('change', function() {
|
||||
btn.disabled = !cb.checked;
|
||||
btn.classList.toggle('btn-submit-disabled', !cb.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 위치 정보 -->
|
||||
<section id="location" class="section">
|
||||
<div class="container">
|
||||
|
||||
46
i18n/en.json
46
i18n/en.json
@ -615,6 +615,44 @@
|
||||
"naverMap": "Naver Map",
|
||||
"googleMap": "Google Maps"
|
||||
},
|
||||
"form": {
|
||||
"title": "Online Inquiry",
|
||||
"desc": "Fill out the form below and we'll get back to you promptly",
|
||||
"name": "Name <span class=\"required\">*</span>",
|
||||
"namePlaceholder": "John Doe",
|
||||
"email": "Email <span class=\"required\">*</span>",
|
||||
"phone": "Phone Number",
|
||||
"service": "Inquiry Type <span class=\"required\">*</span>",
|
||||
"serviceDefault": "Please select",
|
||||
"serviceRental": "Studio Rental",
|
||||
"serviceVtuber": "VTuber Production",
|
||||
"serviceMocap": "Motion Capture",
|
||||
"serviceMV": "Music Video Production",
|
||||
"servicePartner": "Partnership",
|
||||
"serviceOther": "Other",
|
||||
"message": "Message <span class=\"required\">*</span>",
|
||||
"messagePlaceholder": "Please describe your project, preferred schedule, etc.",
|
||||
"sensitiveWarning": "※ Please do not include sensitive information such as national ID or bank account numbers.",
|
||||
"privacyAgree": "I agree to the collection and use of personal information. (Required)",
|
||||
"privacyView": "View Privacy Policy",
|
||||
"privacyPurpose": "Purpose: Inquiry processing and response",
|
||||
"privacyItems": "Items collected: Name, email, phone number, message",
|
||||
"privacyPeriod": "Retention: Automatically deleted after 7 days",
|
||||
"submit": "Send Inquiry",
|
||||
"reset": "Reset",
|
||||
"privacyModalTitle": "Personal Information Collection & Use Notice",
|
||||
"privacyM1Title": "1. Purpose of Collection",
|
||||
"privacyM1Desc": "Processing customer inquiries and providing responses",
|
||||
"privacyM2Title": "2. Items Collected",
|
||||
"privacyM2Required": "Required: Name, email, inquiry type, message",
|
||||
"privacyM2Optional": "Optional: Phone number",
|
||||
"privacyM3Title": "3. Retention Period",
|
||||
"privacyM3Desc": "Data is automatically deleted 7 days after the inquiry is received.",
|
||||
"privacyM4Title": "4. Right to Refuse",
|
||||
"privacyM4Desc": "You have the right to refuse the collection and use of personal information. However, refusal will prevent inquiry submission.",
|
||||
"privacyM5Title": "5. Third-Party Provision",
|
||||
"privacyM5Desc": "Inquiry content is transmitted through a secure channel and may pass through overseas servers. It will not be used for purposes other than inquiry processing."
|
||||
},
|
||||
"cta": {
|
||||
"title": "Reservations & Inquiries",
|
||||
"desc": "Make an easy online reservation or check our frequently asked questions",
|
||||
@ -626,10 +664,16 @@
|
||||
"sending": "Sending...",
|
||||
"sendSuccess": "Your inquiry has been sent successfully. We will contact you shortly.",
|
||||
"sendError": "An error occurred while sending. Please try again.",
|
||||
"errorTitle": "Failed to send",
|
||||
"errorDesc": "Your inquiry could not be sent due to a temporary server error. Please contact us directly using the methods below.",
|
||||
"errorEmail": "Email",
|
||||
"errorPhone": "Phone",
|
||||
"errorDiscord": "Contact us on Discord",
|
||||
"resetConfirm": "All entered data will be deleted. Continue?",
|
||||
"invalidEmail": "Please enter a valid email address.",
|
||||
"invalidPhone": "Please enter a valid phone number.",
|
||||
"required": "This field is required."
|
||||
"required": "This field is required.",
|
||||
"privacyRequired": "Please agree to the collection and use of personal information."
|
||||
}
|
||||
},
|
||||
"qna": {
|
||||
|
||||
46
i18n/ja.json
46
i18n/ja.json
@ -615,6 +615,44 @@
|
||||
"naverMap": "Naver地図",
|
||||
"googleMap": "Google マップ"
|
||||
},
|
||||
"form": {
|
||||
"title": "オンラインお問い合わせ",
|
||||
"desc": "以下のフォームにご記入いただければ、迅速にご返答いたします",
|
||||
"name": "お名前 <span class=\"required\">*</span>",
|
||||
"namePlaceholder": "山田太郎",
|
||||
"email": "メールアドレス <span class=\"required\">*</span>",
|
||||
"phone": "電話番号",
|
||||
"service": "お問い合わせ種類 <span class=\"required\">*</span>",
|
||||
"serviceDefault": "選択してください",
|
||||
"serviceRental": "スタジオレンタル",
|
||||
"serviceVtuber": "VTuber制作",
|
||||
"serviceMocap": "モーションキャプチャー撮影",
|
||||
"serviceMV": "ミュージックビデオ制作",
|
||||
"servicePartner": "提携・協力",
|
||||
"serviceOther": "その他",
|
||||
"message": "お問い合わせ内容 <span class=\"required\">*</span>",
|
||||
"messagePlaceholder": "プロジェクトの内容、ご希望の日程などをご自由にお書きください",
|
||||
"sensitiveWarning": "※ お問い合わせ内容にマイナンバー、口座番号等の機密個人情報を含めないようご注意ください。",
|
||||
"privacyAgree": "個人情報の収集及び利用に同意します。(必須)",
|
||||
"privacyView": "プライバシーポリシーを見る",
|
||||
"privacyPurpose": "収集目的:お問い合わせ受付及び回答",
|
||||
"privacyItems": "収集項目:氏名、メール、電話番号、お問い合わせ内容",
|
||||
"privacyPeriod": "保管期間:7日後に自動削除",
|
||||
"submit": "お問い合わせを送信",
|
||||
"reset": "リセット",
|
||||
"privacyModalTitle": "個人情報の収集及び利用に関するご案内",
|
||||
"privacyM1Title": "1. 収集目的",
|
||||
"privacyM1Desc": "お客様のお問い合わせ受付及び回答の提供",
|
||||
"privacyM2Title": "2. 収集項目",
|
||||
"privacyM2Required": "必須:氏名、メールアドレス、お問い合わせ種類、お問い合わせ内容",
|
||||
"privacyM2Optional": "任意:電話番号",
|
||||
"privacyM3Title": "3. 保管及び利用期間",
|
||||
"privacyM3Desc": "お問い合わせ受付日から7日間保管後、自動的に削除されます。",
|
||||
"privacyM4Title": "4. 同意拒否の権利",
|
||||
"privacyM4Desc": "個人情報の収集及び利用への同意を拒否する権利があります。ただし、同意を拒否された場合、お問い合わせの受付ができません。",
|
||||
"privacyM5Title": "5. 第三者提供",
|
||||
"privacyM5Desc": "お問い合わせ内容はセキュアチャネルを通じて送信され、海外サーバーを経由する場合があります。お問い合わせ処理目的以外には使用されません。"
|
||||
},
|
||||
"cta": {
|
||||
"title": "ご予約・お問い合わせ",
|
||||
"desc": "簡単なオンライン予約またはよくある質問をご確認ください",
|
||||
@ -626,10 +664,16 @@
|
||||
"sending": "送信中...",
|
||||
"sendSuccess": "お問い合わせが正常に送信されました。まもなくご連絡いたします。",
|
||||
"sendError": "送信中にエラーが発生しました。もう一度お試しください。",
|
||||
"errorTitle": "送信に失敗しました",
|
||||
"errorDesc": "一時的なサーバーエラーによりお問い合わせを送信できませんでした。以下の方法で直接ご連絡ください。",
|
||||
"errorEmail": "メール",
|
||||
"errorPhone": "電話",
|
||||
"errorDiscord": "Discordサーバーでお問い合わせ",
|
||||
"resetConfirm": "入力内容がすべて削除されます。続行しますか?",
|
||||
"invalidEmail": "正しいメールアドレスを入力してください。",
|
||||
"invalidPhone": "正しい電話番号を入力してください。",
|
||||
"required": "必須入力項目です。"
|
||||
"required": "必須入力項目です。",
|
||||
"privacyRequired": "個人情報の収集及び利用に同意してください。"
|
||||
}
|
||||
},
|
||||
"qna": {
|
||||
|
||||
46
i18n/ko.json
46
i18n/ko.json
@ -615,6 +615,44 @@
|
||||
"naverMap": "네이버 지도",
|
||||
"googleMap": "구글 맵"
|
||||
},
|
||||
"form": {
|
||||
"title": "온라인 문의",
|
||||
"desc": "아래 양식을 작성하시면 빠르게 답변 드리겠습니다",
|
||||
"name": "이름 <span class=\"required\">*</span>",
|
||||
"namePlaceholder": "홍길동",
|
||||
"email": "이메일 <span class=\"required\">*</span>",
|
||||
"phone": "전화번호",
|
||||
"service": "문의 유형 <span class=\"required\">*</span>",
|
||||
"serviceDefault": "선택해주세요",
|
||||
"serviceRental": "스튜디오 대관",
|
||||
"serviceVtuber": "VTuber 제작",
|
||||
"serviceMocap": "모션캡쳐 촬영",
|
||||
"serviceMV": "뮤직비디오 제작",
|
||||
"servicePartner": "제휴/협력",
|
||||
"serviceOther": "기타",
|
||||
"message": "문의 내용 <span class=\"required\">*</span>",
|
||||
"messagePlaceholder": "프로젝트 내용, 희망 일정 등을 자유롭게 작성해주세요",
|
||||
"sensitiveWarning": "※ 주민등록번호, 계좌번호 등 민감한 개인정보를 입력하지 마세요.",
|
||||
"privacyAgree": "개인정보 수집 및 이용에 동의합니다. (필수)",
|
||||
"privacyView": "개인정보 처리방침 보기",
|
||||
"privacyPurpose": "수집 목적: 문의 접수 및 답변",
|
||||
"privacyItems": "수집 항목: 이름, 이메일, 전화번호, 문의 내용",
|
||||
"privacyPeriod": "보유 기간: 7일 후 자동 파기",
|
||||
"submit": "문의 보내기",
|
||||
"reset": "초기화",
|
||||
"privacyModalTitle": "개인정보 수집 및 이용 안내",
|
||||
"privacyM1Title": "1. 수집 목적",
|
||||
"privacyM1Desc": "고객 문의 접수 및 답변 제공",
|
||||
"privacyM2Title": "2. 수집 항목",
|
||||
"privacyM2Required": "필수: 이름, 이메일, 문의 유형, 문의 내용",
|
||||
"privacyM2Optional": "선택: 전화번호",
|
||||
"privacyM3Title": "3. 보유 및 이용 기간",
|
||||
"privacyM3Desc": "문의 접수일로부터 7일간 보관 후 자동 파기됩니다.",
|
||||
"privacyM4Title": "4. 동의 거부 권리",
|
||||
"privacyM4Desc": "개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다. 다만, 동의를 거부하실 경우 문의 접수가 불가합니다.",
|
||||
"privacyM5Title": "5. 제3자 제공",
|
||||
"privacyM5Desc": "문의 내용은 보안 채널을 통해 전달되며, 해외 서버를 경유할 수 있습니다. 문의 처리 목적 외에는 사용되지 않습니다."
|
||||
},
|
||||
"cta": {
|
||||
"title": "예약 및 문의",
|
||||
"desc": "간편한 온라인 예약 또는 자주 묻는 질문을 확인해보세요",
|
||||
@ -626,10 +664,16 @@
|
||||
"sending": "전송 중...",
|
||||
"sendSuccess": "문의가 성공적으로 전송되었습니다. 빠른 시일 내에 연락드리겠습니다.",
|
||||
"sendError": "전송 중 오류가 발생했습니다. 다시 시도해 주세요.",
|
||||
"errorTitle": "전송에 실패했습니다",
|
||||
"errorDesc": "일시적인 서버 오류로 문의가 전송되지 않았습니다. 아래 방법으로 직접 연락해 주세요.",
|
||||
"errorEmail": "이메일",
|
||||
"errorPhone": "전화",
|
||||
"errorDiscord": "디스코드 서버에서 문의",
|
||||
"resetConfirm": "입력한 내용이 모두 삭제됩니다. 계속하시겠습니까?",
|
||||
"invalidEmail": "올바른 이메일 형식을 입력해 주세요.",
|
||||
"invalidPhone": "올바른 전화번호 형식을 입력해 주세요.",
|
||||
"required": "필수 입력 항목입니다."
|
||||
"required": "필수 입력 항목입니다.",
|
||||
"privacyRequired": "개인정보 수집 및 이용에 동의해 주세요."
|
||||
}
|
||||
},
|
||||
"qna": {
|
||||
|
||||
46
i18n/zh.json
46
i18n/zh.json
@ -615,6 +615,44 @@
|
||||
"naverMap": "Naver地图",
|
||||
"googleMap": "Google地图"
|
||||
},
|
||||
"form": {
|
||||
"title": "在线咨询",
|
||||
"desc": "请填写以下表单,我们将尽快回复",
|
||||
"name": "姓名 <span class=\"required\">*</span>",
|
||||
"namePlaceholder": "张三",
|
||||
"email": "邮箱 <span class=\"required\">*</span>",
|
||||
"phone": "电话号码",
|
||||
"service": "咨询类型 <span class=\"required\">*</span>",
|
||||
"serviceDefault": "请选择",
|
||||
"serviceRental": "工作室租赁",
|
||||
"serviceVtuber": "VTuber制作",
|
||||
"serviceMocap": "动作捕捉拍摄",
|
||||
"serviceMV": "MV制作",
|
||||
"servicePartner": "合作/联盟",
|
||||
"serviceOther": "其他",
|
||||
"message": "咨询内容 <span class=\"required\">*</span>",
|
||||
"messagePlaceholder": "请自由填写项目内容、希望的日程等",
|
||||
"sensitiveWarning": "※ 请勿在咨询内容中包含身份证号、银行账号等敏感个人信息。",
|
||||
"privacyAgree": "同意收集和使用个人信息。(必填)",
|
||||
"privacyView": "查看隐私政策",
|
||||
"privacyPurpose": "收集目的:咨询受理及回复",
|
||||
"privacyItems": "收集项目:姓名、邮箱、电话号码、咨询内容",
|
||||
"privacyPeriod": "保留期限:7天后自动删除",
|
||||
"submit": "发送咨询",
|
||||
"reset": "重置",
|
||||
"privacyModalTitle": "个人信息收集及使用须知",
|
||||
"privacyM1Title": "1. 收集目的",
|
||||
"privacyM1Desc": "客户咨询受理及回复",
|
||||
"privacyM2Title": "2. 收集项目",
|
||||
"privacyM2Required": "必填:姓名、邮箱、咨询类型、咨询内容",
|
||||
"privacyM2Optional": "选填:电话号码",
|
||||
"privacyM3Title": "3. 保留及使用期限",
|
||||
"privacyM3Desc": "自咨询受理之日起保留7天后自动删除。",
|
||||
"privacyM4Title": "4. 拒绝同意的权利",
|
||||
"privacyM4Desc": "您有权拒绝个人信息的收集和使用。但拒绝后将无法提交咨询。",
|
||||
"privacyM5Title": "5. 第三方提供",
|
||||
"privacyM5Desc": "咨询内容通过安全渠道传送,可能经由海外服务器。不会用于咨询处理以外的目的。"
|
||||
},
|
||||
"cta": {
|
||||
"title": "预约与咨询",
|
||||
"desc": "便捷的在线预约或查看常见问题",
|
||||
@ -626,10 +664,16 @@
|
||||
"sending": "发送中...",
|
||||
"sendSuccess": "咨询已成功发送。我们将尽快与您联系。",
|
||||
"sendError": "发送过程中出现错误,请重试。",
|
||||
"errorTitle": "发送失败",
|
||||
"errorDesc": "由于临时服务器错误,您的咨询未能发送。请通过以下方式直接联系我们。",
|
||||
"errorEmail": "电子邮箱",
|
||||
"errorPhone": "电话",
|
||||
"errorDiscord": "通过Discord服务器咨询",
|
||||
"resetConfirm": "所有输入内容将被删除。是否继续?",
|
||||
"invalidEmail": "请输入正确的邮箱地址。",
|
||||
"invalidPhone": "请输入正确的电话号码。",
|
||||
"required": "此为必填项。"
|
||||
"required": "此为必填项。",
|
||||
"privacyRequired": "请同意收集和使用个人信息。"
|
||||
}
|
||||
},
|
||||
"qna": {
|
||||
|
||||
178
ja/contact.html
178
ja/contact.html
@ -197,6 +197,184 @@
|
||||
</section>
|
||||
|
||||
|
||||
<!-- 온라인 문의 폼 -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 data-i18n="contact.form.title">온라인 문의</h2>
|
||||
<p data-i18n="contact.form.desc">아래 양식을 작성하시면 빠르게 답변 드리겠습니다</p>
|
||||
</div>
|
||||
|
||||
<div class="contact-form-wrapper">
|
||||
<form id="contactForm" class="contact-form" novalidate>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="name" data-i18n="contact.form.name">이름 <span class="required">*</span></label>
|
||||
<input type="text" id="name" name="name" required placeholder="홍길동" data-i18n-placeholder="contact.form.namePlaceholder">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email" data-i18n="contact.form.email">이메일 <span class="required">*</span></label>
|
||||
<input type="email" id="email" name="email" required placeholder="example@email.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="phone" data-i18n="contact.form.phone">전화번호</label>
|
||||
<input type="tel" id="phone" name="phone" placeholder="010-0000-0000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service" data-i18n="contact.form.service">문의 유형 <span class="required">*</span></label>
|
||||
<select id="service" name="service" required>
|
||||
<option value="" data-i18n="contact.form.serviceDefault">선택해주세요</option>
|
||||
<option value="studio_rental" data-i18n="contact.form.serviceRental">스튜디오 대관</option>
|
||||
<option value="vtuber" data-i18n="contact.form.serviceVtuber">VTuber 제작</option>
|
||||
<option value="mocap" data-i18n="contact.form.serviceMocap">모션캡쳐 촬영</option>
|
||||
<option value="music_video" data-i18n="contact.form.serviceMV">뮤직비디오 제작</option>
|
||||
<option value="partnership" data-i18n="contact.form.servicePartner">제휴/협력</option>
|
||||
<option value="other" data-i18n="contact.form.serviceOther">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message" data-i18n="contact.form.message">문의 내용 <span class="required">*</span></label>
|
||||
<textarea id="message" name="message" required rows="5" placeholder="프로젝트 내용, 희망 일정 등을 자유롭게 작성해주세요" data-i18n-placeholder="contact.form.messagePlaceholder"></textarea>
|
||||
<p class="field-hint" data-i18n="contact.form.sensitiveWarning">※ 주민등록번호, 계좌번호 등 민감한 개인정보를 입력하지 마세요.</p>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="privacyConsent" name="privacyConsent" required>
|
||||
<span class="checkmark"></span>
|
||||
<span data-i18n="contact.form.privacyAgree">개인정보 수집 및 이용에 동의합니다. (필수)</span>
|
||||
</label>
|
||||
<a href="javascript:void(0)" class="privacy-link" onclick="openPrivacyModal()" data-i18n="contact.form.privacyView">개인정보 처리방침 보기</a>
|
||||
</div>
|
||||
|
||||
<div class="privacy-summary">
|
||||
<ul>
|
||||
<li data-i18n="contact.form.privacyPurpose">수집 목적: 문의 접수 및 답변</li>
|
||||
<li data-i18n="contact.form.privacyItems">수집 항목: 이름, 이메일, 전화번호, 문의 내용</li>
|
||||
<li data-i18n="contact.form.privacyPeriod">보유 기간: 7일 후 자동 파기</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-submit">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary btn-lg btn-submit-disabled" disabled data-i18n="contact.form.submit">문의 보내기</button>
|
||||
<button type="reset" class="btn btn-outline btn-lg" data-i18n="contact.form.reset">초기화</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 개인정보처리방침 모달 -->
|
||||
<div id="privacyModal" class="modal" onclick="if(event.target===this)closePrivacyModal()" role="dialog" aria-modal="true" aria-labelledby="privacyModalTitle">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="privacyModalTitle">個人情報の収集及び利用に関する同意書</h3>
|
||||
<button class="modal-close" onclick="closePrivacyModal()" aria-label="閉じる">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Mingle Studio(以下「当社」)は、韓国「個人情報保護法」に基づき、個人情報の収集及び利用について同意を求めます。</p>
|
||||
|
||||
<h4>1. 収集及び利用目的</h4>
|
||||
<p>当社は以下の目的で個人情報を処理します。</p>
|
||||
<ul>
|
||||
<li>お客様からのお問い合わせ受付及び回答</li>
|
||||
<li>サービス相談及び見積もりのご案内</li>
|
||||
<li>お問い合わせ処理結果の通知</li>
|
||||
</ul>
|
||||
|
||||
<h4>2. 収集する個人情報の項目</h4>
|
||||
<ul>
|
||||
<li><strong>必須項目:</strong>氏名、メールアドレス、お問い合わせ種類、お問い合わせ内容</li>
|
||||
<li><strong>任意項目:</strong>電話番号</li>
|
||||
</ul>
|
||||
<p style="color: #ef4444; font-size: 0.85rem;">※ お問い合わせ内容にマイナンバー、口座番号等の機密個人情報を含めないようご注意ください。</p>
|
||||
|
||||
<h4>3. 保有及び利用期間</h4>
|
||||
<p>収集された個人情報は<strong>お問い合わせ受付日から7日間</strong>保有し、保有期間経過後に自動的に破棄されます。ただし、関係法令による保存義務がある場合は、当該法令が定める期間保存します。</p>
|
||||
|
||||
<h4>4. 破棄手順及び方法</h4>
|
||||
<ul>
|
||||
<li><strong>破棄手順:</strong>保有期間を経過した個人情報は、自動削除システムにより遅滞なく破棄されます。</li>
|
||||
<li><strong>破棄方法:</strong>電子ファイル形式の情報は、復元・再生できないよう完全に削除します。</li>
|
||||
</ul>
|
||||
|
||||
<h4>5. 第三者への提供及び国外移転</h4>
|
||||
<p>当社は原則として利用者の個人情報を第三者に提供しません。ただし、お問い合わせ処理のため、以下のとおり個人情報が国外に移転されます。(「個人情報保護法」第28条の8)</p>
|
||||
<ul>
|
||||
<li><strong>移転先の国及び受領者:</strong>米国 / Discord Inc.</li>
|
||||
<li><strong>移転時期及び方法:</strong>お問い合わせ送信時に専用API(インターネット回線)を通じてリアルタイム送信</li>
|
||||
<li><strong>移転項目:</strong>氏名、メール、電話番号(任意)、お問い合わせ種類、お問い合わせ内容</li>
|
||||
<li><strong>移転目的:</strong>業務用コラボレーションツール(Discord)による迅速なお問い合わせ確認及び対応</li>
|
||||
<li><strong>保有及び利用期間:</strong>送信後7日間保管後、自動破棄</li>
|
||||
</ul>
|
||||
|
||||
<h4>6. 情報主体の権利</h4>
|
||||
<p>利用者は個人情報の主体として、以下の権利を行使できます。</p>
|
||||
<ul>
|
||||
<li>個人情報の閲覧要求</li>
|
||||
<li>誤り等の訂正要求</li>
|
||||
<li>削除要求</li>
|
||||
<li>処理停止要求</li>
|
||||
</ul>
|
||||
<p>上記の権利行使は、下記の個人情報保護責任者にご連絡ください。当社は遅滞なく対応いたします。</p>
|
||||
|
||||
<h4>7. 安全性確保措置</h4>
|
||||
<p>当社は個人情報の安全性確保のため、以下の措置を講じています。</p>
|
||||
<ul>
|
||||
<li>送信データの暗号化(HTTPS/TLS)</li>
|
||||
<li>アクセス権限の制限(指定された管理者のみ閲覧可能)</li>
|
||||
<li>自動削除システムの運用(7日後に破棄)</li>
|
||||
</ul>
|
||||
|
||||
<h4>8. 同意拒否の権利及び不利益</h4>
|
||||
<p>利用者は個人情報の収集及び利用への同意を拒否する権利があります。ただし、必須項目への同意を拒否された場合、お問い合わせサービスのご利用が制限されます。</p>
|
||||
|
||||
<h4>9. 個人情報保護責任者</h4>
|
||||
<ul>
|
||||
<li><strong>氏名:</strong>キム・ヒジン</li>
|
||||
<li><strong>役職:</strong>代表</li>
|
||||
<li><strong>メール:</strong>minglestudio@minglestudio.co.kr</li>
|
||||
<li><strong>電話:</strong>+82-10-9288-9190</li>
|
||||
</ul>
|
||||
|
||||
<h4>10. 本方針の変更</h4>
|
||||
<p>この個人情報処理方針は2026年3月4日から適用されます。変更がある場合は、本ウェブサイトを通じてお知らせします。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function openPrivacyModal() {
|
||||
var m = document.getElementById('privacyModal');
|
||||
if (!m) return;
|
||||
m.classList.add('active');
|
||||
m.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
function closePrivacyModal() {
|
||||
var m = document.getElementById('privacyModal');
|
||||
if (!m) return;
|
||||
m.classList.remove('active');
|
||||
m.style.display = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
// 체크박스 → 제출 버튼 활성화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var cb = document.getElementById('privacyConsent');
|
||||
var btn = document.getElementById('submitBtn');
|
||||
if (cb && btn) {
|
||||
cb.addEventListener('change', function() {
|
||||
btn.disabled = !cb.checked;
|
||||
btn.classList.toggle('btn-submit-disabled', !cb.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 위치 정보 -->
|
||||
<section id="location" class="section">
|
||||
<div class="container">
|
||||
|
||||
184
js/contact.js
184
js/contact.js
@ -52,6 +52,13 @@ async function handleFormSubmit(e) {
|
||||
showNotification(_t('contact.js.checkInput', '입력 정보를 확인해 주세요.'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 개인정보 동의 확인
|
||||
const privacyCheckbox = document.getElementById('privacyConsent');
|
||||
if (privacyCheckbox && !privacyCheckbox.checked) {
|
||||
showNotification(_t('contact.js.privacyRequired', '개인정보 수집 및 이용에 동의해 주세요.'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 제출 버튼 비활성화
|
||||
const originalText = submitBtn.textContent;
|
||||
@ -72,7 +79,7 @@ async function handleFormSubmit(e) {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
showNotification(_t('contact.js.sendError', '전송 중 오류가 발생했습니다. 다시 시도해 주세요.'), 'error');
|
||||
showApiErrorFallback();
|
||||
} finally {
|
||||
// 버튼 복원
|
||||
submitBtn.textContent = originalText;
|
||||
@ -80,19 +87,70 @@ async function handleFormSubmit(e) {
|
||||
}
|
||||
}
|
||||
|
||||
// 서버 전송 (mailto 기반 폴백)
|
||||
// 서버 전송 (API)
|
||||
async function submitContactForm(data) {
|
||||
// mailto 링크로 이메일 클라이언트 열기
|
||||
const subject = encodeURIComponent(`[밍글 스튜디오 문의] ${data.name || '웹사이트 문의'}`);
|
||||
const body = encodeURIComponent(
|
||||
`이름: ${data.name || ''}\n` +
|
||||
`이메일: ${data.email || ''}\n` +
|
||||
`전화번호: ${data.phone || ''}\n` +
|
||||
`문의 유형: ${data.service || ''}\n` +
|
||||
`\n문의 내용:\n${data.message || ''}`
|
||||
);
|
||||
window.location.href = `mailto:mingle_studio@naver.com?subject=${subject}&body=${body}`;
|
||||
return { success: true, message: '이메일 클라이언트가 열렸습니다.' };
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: data.name || '',
|
||||
email: data.email || '',
|
||||
phone: data.phone || '',
|
||||
service: data.service || '',
|
||||
message: data.message || ''
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.error || '전송 실패');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// API 실패 시 대체 연락 안내
|
||||
function showApiErrorFallback() {
|
||||
// 기존 fallback 제거
|
||||
const existing = document.getElementById('apiErrorFallback');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const fallback = document.createElement('div');
|
||||
fallback.id = 'apiErrorFallback';
|
||||
fallback.className = 'api-error-fallback';
|
||||
fallback.innerHTML = `
|
||||
<div class="api-error-content">
|
||||
<button class="api-error-close" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||
<div class="api-error-icon"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
||||
<h3>${_t('contact.js.errorTitle', '전송에 실패했습니다')}</h3>
|
||||
<p>${_t('contact.js.errorDesc', '일시적인 서버 오류로 문의가 전송되지 않았습니다. 아래 방법으로 직접 연락해 주세요.')}</p>
|
||||
<div class="api-error-contacts">
|
||||
<a href="mailto:minglestudio@minglestudio.co.kr" class="api-error-method">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<div>
|
||||
<strong>${_t('contact.js.errorEmail', '이메일')}</strong>
|
||||
<span>minglestudio@minglestudio.co.kr</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="tel:010-9288-9190" class="api-error-method">
|
||||
<i class="fa-solid fa-phone"></i>
|
||||
<div>
|
||||
<strong>${_t('contact.js.errorPhone', '전화')}</strong>
|
||||
<span>010-9288-9190</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://discord.gg/minglestudio" target="_blank" rel="noopener" class="api-error-method">
|
||||
<i class="fa-brands fa-discord"></i>
|
||||
<div>
|
||||
<strong>Discord</strong>
|
||||
<span>${_t('contact.js.errorDiscord', '디스코드 서버에서 문의')}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(fallback);
|
||||
}
|
||||
|
||||
// 폼 리셋 처리
|
||||
@ -104,10 +162,17 @@ function handleFormReset(e) {
|
||||
// 에러 메시지 제거
|
||||
const errorElements = document.querySelectorAll('.field-error');
|
||||
errorElements.forEach(el => el.remove());
|
||||
|
||||
|
||||
// 필드 에러 스타일 제거
|
||||
const fields = document.querySelectorAll('.form-group input, .form-group select, .form-group textarea');
|
||||
fields.forEach(field => field.classList.remove('error'));
|
||||
|
||||
// 제출 버튼 재비활성화
|
||||
const btn = document.getElementById('submitBtn');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.classList.add('btn-submit-disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,58 +292,57 @@ function formatPhoneNumber(e) {
|
||||
// common.js의 showNotification 사용
|
||||
const showNotification = window.commonUtils?.showNotification || function() {};
|
||||
|
||||
// 모달 전역 함수 (onclick에서도 호출 가능)
|
||||
let _modalLastFocus = null;
|
||||
|
||||
function openPrivacyModal() {
|
||||
const modal = document.getElementById('privacyModal');
|
||||
if (!modal) return;
|
||||
_modalLastFocus = document.activeElement;
|
||||
modal.classList.add('active');
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.style.position = 'fixed';
|
||||
document.body.style.width = '100%';
|
||||
document.body.style.top = `-${window.scrollY}px`;
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
if (closeBtn) closeBtn.focus();
|
||||
}
|
||||
|
||||
function closePrivacyModal() {
|
||||
const modal = document.getElementById('privacyModal');
|
||||
if (!modal) return;
|
||||
modal.classList.remove('active');
|
||||
modal.style.display = '';
|
||||
const scrollY = document.body.style.top;
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.position = '';
|
||||
document.body.style.width = '';
|
||||
document.body.style.top = '';
|
||||
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
||||
if (_modalLastFocus) _modalLastFocus.focus();
|
||||
}
|
||||
|
||||
// 모달 초기화
|
||||
function initModal() {
|
||||
const modal = document.getElementById('privacyModal');
|
||||
const privacyLink = document.querySelector('.privacy-link');
|
||||
const closeBtn = modal?.querySelector('.modal-close');
|
||||
|
||||
let lastFocusedElement = null;
|
||||
|
||||
function openModal() {
|
||||
lastFocusedElement = document.activeElement;
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.style.position = 'fixed';
|
||||
document.body.style.width = '100%';
|
||||
document.body.style.top = `-${window.scrollY}px`;
|
||||
// 포커스를 모달 닫기 버튼으로 이동
|
||||
if (closeBtn) closeBtn.focus();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.classList.remove('active');
|
||||
const scrollY = document.body.style.top;
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.position = '';
|
||||
document.body.style.width = '';
|
||||
document.body.style.top = '';
|
||||
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
||||
// 포커스 복원
|
||||
if (lastFocusedElement) lastFocusedElement.focus();
|
||||
}
|
||||
|
||||
if (privacyLink && modal) {
|
||||
privacyLink.addEventListener('click', function(e) {
|
||||
// 이벤트 위임 (백업 — HTML onclick이 주요 트리거)
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.privacy-link')) {
|
||||
e.preventDefault();
|
||||
openModal();
|
||||
});
|
||||
}
|
||||
openPrivacyModal();
|
||||
}
|
||||
if (e.target.closest('.modal-close')) {
|
||||
closePrivacyModal();
|
||||
}
|
||||
if (e.target.id === 'privacyModal') {
|
||||
closePrivacyModal();
|
||||
}
|
||||
});
|
||||
|
||||
if (closeBtn && modal) {
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
if (modal) {
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// ESC 키로 모달 닫기
|
||||
document.addEventListener('keydown', function(e) {
|
||||
const modal = document.getElementById('privacyModal');
|
||||
if (e.key === 'Escape' && modal?.classList.contains('active')) {
|
||||
closeModal();
|
||||
closePrivacyModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
178
zh/contact.html
178
zh/contact.html
@ -197,6 +197,184 @@
|
||||
</section>
|
||||
|
||||
|
||||
<!-- 온라인 문의 폼 -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 data-i18n="contact.form.title">온라인 문의</h2>
|
||||
<p data-i18n="contact.form.desc">아래 양식을 작성하시면 빠르게 답변 드리겠습니다</p>
|
||||
</div>
|
||||
|
||||
<div class="contact-form-wrapper">
|
||||
<form id="contactForm" class="contact-form" novalidate>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="name" data-i18n="contact.form.name">이름 <span class="required">*</span></label>
|
||||
<input type="text" id="name" name="name" required placeholder="홍길동" data-i18n-placeholder="contact.form.namePlaceholder">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email" data-i18n="contact.form.email">이메일 <span class="required">*</span></label>
|
||||
<input type="email" id="email" name="email" required placeholder="example@email.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="phone" data-i18n="contact.form.phone">전화번호</label>
|
||||
<input type="tel" id="phone" name="phone" placeholder="010-0000-0000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service" data-i18n="contact.form.service">문의 유형 <span class="required">*</span></label>
|
||||
<select id="service" name="service" required>
|
||||
<option value="" data-i18n="contact.form.serviceDefault">선택해주세요</option>
|
||||
<option value="studio_rental" data-i18n="contact.form.serviceRental">스튜디오 대관</option>
|
||||
<option value="vtuber" data-i18n="contact.form.serviceVtuber">VTuber 제작</option>
|
||||
<option value="mocap" data-i18n="contact.form.serviceMocap">모션캡쳐 촬영</option>
|
||||
<option value="music_video" data-i18n="contact.form.serviceMV">뮤직비디오 제작</option>
|
||||
<option value="partnership" data-i18n="contact.form.servicePartner">제휴/협력</option>
|
||||
<option value="other" data-i18n="contact.form.serviceOther">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message" data-i18n="contact.form.message">문의 내용 <span class="required">*</span></label>
|
||||
<textarea id="message" name="message" required rows="5" placeholder="프로젝트 내용, 희망 일정 등을 자유롭게 작성해주세요" data-i18n-placeholder="contact.form.messagePlaceholder"></textarea>
|
||||
<p class="field-hint" data-i18n="contact.form.sensitiveWarning">※ 주민등록번호, 계좌번호 등 민감한 개인정보를 입력하지 마세요.</p>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="privacyConsent" name="privacyConsent" required>
|
||||
<span class="checkmark"></span>
|
||||
<span data-i18n="contact.form.privacyAgree">개인정보 수집 및 이용에 동의합니다. (필수)</span>
|
||||
</label>
|
||||
<a href="javascript:void(0)" class="privacy-link" onclick="openPrivacyModal()" data-i18n="contact.form.privacyView">개인정보 처리방침 보기</a>
|
||||
</div>
|
||||
|
||||
<div class="privacy-summary">
|
||||
<ul>
|
||||
<li data-i18n="contact.form.privacyPurpose">수집 목적: 문의 접수 및 답변</li>
|
||||
<li data-i18n="contact.form.privacyItems">수집 항목: 이름, 이메일, 전화번호, 문의 내용</li>
|
||||
<li data-i18n="contact.form.privacyPeriod">보유 기간: 7일 후 자동 파기</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-submit">
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary btn-lg btn-submit-disabled" disabled data-i18n="contact.form.submit">문의 보내기</button>
|
||||
<button type="reset" class="btn btn-outline btn-lg" data-i18n="contact.form.reset">초기화</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 개인정보처리방침 모달 -->
|
||||
<div id="privacyModal" class="modal" onclick="if(event.target===this)closePrivacyModal()" role="dialog" aria-modal="true" aria-labelledby="privacyModalTitle">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="privacyModalTitle">个人信息收集及使用同意书</h3>
|
||||
<button class="modal-close" onclick="closePrivacyModal()" aria-label="关闭">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Mingle Studio(以下简称"公司")根据韩国《个人信息保护法》,就个人信息的收集和使用征求您的同意。</p>
|
||||
|
||||
<h4>1. 收集及使用目的</h4>
|
||||
<p>公司为以下目的处理个人信息:</p>
|
||||
<ul>
|
||||
<li>受理并回复客户咨询</li>
|
||||
<li>服务咨询及报价指导</li>
|
||||
<li>咨询处理结果通知</li>
|
||||
</ul>
|
||||
|
||||
<h4>2. 收集的个人信息项目</h4>
|
||||
<ul>
|
||||
<li><strong>必填项目:</strong>姓名、电子邮箱、咨询类型、咨询内容</li>
|
||||
<li><strong>选填项目:</strong>电话号码</li>
|
||||
</ul>
|
||||
<p style="color: #ef4444; font-size: 0.85rem;">※ 请勿在咨询内容中包含身份证号、银行账号等敏感个人信息。</p>
|
||||
|
||||
<h4>3. 保留及使用期限</h4>
|
||||
<p>收集的个人信息自<strong>咨询受理之日起保留7天</strong>,超过保留期限后自动销毁。但根据相关法律法规有保存义务的,按法律规定的期限保存。</p>
|
||||
|
||||
<h4>4. 销毁程序及方法</h4>
|
||||
<ul>
|
||||
<li><strong>销毁程序:</strong>超过保留期限的个人信息由自动删除系统及时销毁。</li>
|
||||
<li><strong>销毁方法:</strong>电子文件形式的信息以无法恢复和再生的方式完全删除。</li>
|
||||
</ul>
|
||||
|
||||
<h4>5. 向第三方提供及跨境转移</h4>
|
||||
<p>公司原则上不向第三方提供用户的个人信息。但为处理咨询,个人信息按以下方式转移至境外(依据《个人信息保护法》第28条之8):</p>
|
||||
<ul>
|
||||
<li><strong>转移国家及接收者:</strong>美国 / Discord Inc.</li>
|
||||
<li><strong>转移时间及方式:</strong>咨询提交时通过专用API(互联网)实时传输</li>
|
||||
<li><strong>转移项目:</strong>姓名、邮箱、电话号码(选填)、咨询类型、咨询内容</li>
|
||||
<li><strong>转移目的:</strong>通过协作工具(Discord)进行快速咨询确认及回复</li>
|
||||
<li><strong>保留及使用期限:</strong>传输后保管7天后自动销毁</li>
|
||||
</ul>
|
||||
|
||||
<h4>6. 信息主体的权利</h4>
|
||||
<p>用户作为个人信息主体,可以行使以下权利:</p>
|
||||
<ul>
|
||||
<li>要求查阅个人信息</li>
|
||||
<li>要求更正错误</li>
|
||||
<li>要求删除</li>
|
||||
<li>要求停止处理</li>
|
||||
</ul>
|
||||
<p>上述权利可通过联系以下个人信息保护负责人行使,公司将及时采取措施。</p>
|
||||
|
||||
<h4>7. 安全保障措施</h4>
|
||||
<p>公司为确保个人信息安全采取以下措施:</p>
|
||||
<ul>
|
||||
<li>传输数据加密(HTTPS/TLS)</li>
|
||||
<li>访问权限限制(仅指定管理员可查看)</li>
|
||||
<li>自动删除系统运营(7天后销毁)</li>
|
||||
</ul>
|
||||
|
||||
<h4>8. 拒绝同意的权利及不利影响</h4>
|
||||
<p>用户有权拒绝个人信息的收集和使用。但拒绝必填项目的同意将限制咨询服务的使用。</p>
|
||||
|
||||
<h4>9. 个人信息保护负责人</h4>
|
||||
<ul>
|
||||
<li><strong>姓名:</strong>金希真</li>
|
||||
<li><strong>职位:</strong>代表</li>
|
||||
<li><strong>邮箱:</strong>minglestudio@minglestudio.co.kr</li>
|
||||
<li><strong>电话:</strong>+82-10-9288-9190</li>
|
||||
</ul>
|
||||
|
||||
<h4>10. 隐私政策的变更</h4>
|
||||
<p>本隐私政策自2026年3月4日起生效。如有变更,将通过本网站公告。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function openPrivacyModal() {
|
||||
var m = document.getElementById('privacyModal');
|
||||
if (!m) return;
|
||||
m.classList.add('active');
|
||||
m.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
function closePrivacyModal() {
|
||||
var m = document.getElementById('privacyModal');
|
||||
if (!m) return;
|
||||
m.classList.remove('active');
|
||||
m.style.display = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
// 체크박스 → 제출 버튼 활성화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var cb = document.getElementById('privacyConsent');
|
||||
var btn = document.getElementById('submitBtn');
|
||||
if (cb && btn) {
|
||||
cb.addEventListener('change', function() {
|
||||
btn.disabled = !cb.checked;
|
||||
btn.classList.toggle('btn-submit-disabled', !cb.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 위치 정보 -->
|
||||
<section id="location" class="section">
|
||||
<div class="container">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user