플레이스토어 출시 준비: 릴리즈 서명, 패키지명 변경, HTTPS 전환, NFC 태그 쓰기 기능 추가

## 릴리즈 빌드 준비
- android/app/build.gradle.kts: 디버그 키로 서명하던 release 빌드 타입을
  android/key.properties 기반 정식 릴리즈 키스토어(upload-keystore.jks) 서명으로 변경
- applicationId를 com.example.school_attendance → com.backsanhighschool.app 로 변경
  (com.example.* 패키지는 구글 플레이 업로드 자체가 거부되기 때문)

## Firebase 재등록
- 새 패키지명(com.backsanhighschool.app)으로 Firebase Android 앱을 새로 등록
- google-services.json, lib/firebase_options.dart, firebase.json을 새 앱 ID 기준으로 재생성

## HTTPS 전환
- lib/config.dart: baseUrl을 http:// → https:// 로 변경
  (서버에 이미 발급되어 있었지만 프록시에 연결이 안 되어 있던 Let's Encrypt
  인증서를 nginx-proxy-manager에 연결하고 HTTP→HTTPS 강제 리다이렉트 설정)
- AndroidManifest.xml: 더 이상 필요 없는 usesCleartextTraffic="true" 제거

## NFC 태그 쓰기 기능 (신규)
- lib/screens/nfc_tag_writer_screen.dart 추가: 관리자가 빈 NFC 스티커에
  "POCKET_번호" 텍스트를 쓰고, 태그 하드웨어 UID를 서버(/api/pockets/register)에
  등록해서 태그 복제(내용만 베낀 위조 태그)를 방지
- lib/nfc_poccket_checkin_screen.dart: 체크인 시 태그 UID를 함께 서버로 전송해서
  등록된 진짜 주머니 번호로 검증하도록 변경
- lib/screens/student_dashboard.dart: 개발자 메뉴에 "NFC 태그 쓰기" 카드 추가,
  기존 "가상 NFC 태깅" 카드는 "NFC 태그"로 이름 변경

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 15:46:51 +09:00
co-authored by Claude Sonnet 5
parent 3f2fb0a57e
commit 7f6448ae9b
9 changed files with 280 additions and 28 deletions
+15 -16
View File
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:http/http.dart' as http;
import 'package:nfc_manager/nfc_manager.dart';
import 'package:nfc_manager/nfc_manager_android.dart';
import 'package:nfc_manager_ndef/nfc_manager_ndef.dart';
import 'package:permission_handler/permission_handler.dart';
import 'config.dart' show baseUrl;
@@ -104,8 +105,8 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
cleanPocketNumber = rawPocketData.trim();
}
// 2. 파이썬 서버로 출석 정보 보내기
await _sendAttendanceToBackend(cleanPocketNumber);
// 2. 파이썬 서버로 출석 정보 보내기 (복제 방지용 태그 UID도 함께 전송)
await _sendAttendanceToBackend(cleanPocketNumber, tagUid: _getTagUid(tag));
} catch (e) {
_showSnackBar("❌ 태그 읽기 실패: $e", Colors.red);
} finally {
@@ -119,8 +120,18 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
);
}
/// 태그의 공장 각인 하드웨어 UID를 16진수 문자열로 반환한다 (쓰기로 바꿀 수 없어 복제 방지 기준으로 씀).
String? _getTagUid(NfcTag tag) {
final id = NfcTagAndroid.from(tag)?.id;
if (id == null || id.isEmpty) return null;
return id.map((b) => b.toRadixString(16).padLeft(2, '0')).join().toUpperCase();
}
/// 🚚 파이썬 백엔드로 출석 데이터 HTTP POST 전송
Future<void> _sendAttendanceToBackend(String pocketNumber) async {
Future<void> _sendAttendanceToBackend(
String pocketNumber, {
String? tagUid,
}) async {
final url = Uri.parse("$baseUrl/attendance");
try {
@@ -131,6 +142,7 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
"studentId": widget.studentId,
"studentName": widget.studentName,
"pocketNumber": pocketNumber, // 주머니 번호 전달
"tagUid": tagUid, // 🔒 서버가 태그 진위를 대조할 하드웨어 UID
}),
);
@@ -287,19 +299,6 @@ class _NfcPocketCheckInScreenState extends State<NfcPocketCheckInScreen> {
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey, fontSize: 14),
),
const SizedBox(height: 40),
// 🧪 [디버그 전용] NFC 태그나 카드가 없을 때 흐름을 테스트하기 위한 가상 태그 버튼
if (!_isProcessing)
OutlinedButton.icon(
onPressed: () => _sendAttendanceToBackend("POCKET_TEST_01"),
icon: const Icon(Icons.science_outlined),
label: const Text("가상 태그로 테스트 (NFC 없이)"),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.purple,
side: const BorderSide(color: Colors.purple),
),
),
],
),
),