플레이스토어 출시 준비: 릴리즈 서명, 패키지명 변경, 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:
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user