diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..b6110a7b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +## Description + + +## To Reproduce + + +## Expected behavior + + +## Error Log +``` +``` + +## Screenshots + + +## Development Environment + - OS: + - Output of `flutter doctor`: + +## Test Environment + - Device(OS): + - App Version or Comit Hash: + +## Additional context + diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..48d5f81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..33ebfd58 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature]" +labels: enhancement +assignees: '' + +--- + +## Related Problem + + +## Describe the Solution + + +## Describe Alternatives + + +## Additional Context + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..4a4d405c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ +## Overview + + +## Changes + + +## Implementaion Method + + +## After Changes + + +## Related Issues + + +## Rollback Scenario + + +## TODO + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 381cd090..4bbb2c7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: push: branches: [dev, main] pull_request: - branches: [dev] + branches: [dev, main] env: FLUTTER_VERSION: "3.13" @@ -34,6 +34,10 @@ jobs: echo "keyAlias=ci" >> android/key.properties - name: Install dependencies run: flutter pub get + - name: Create .env files + run: | + echo "${{ secrets.ENV_PRODUCTION_FILE_CONTENT }}" > .env.production + echo "${{ secrets.ENV_DEVELOPMENT_FILE_CONTENT }}" > .env.development - name: Build APK run: flutter build apk --release --no-tree-shake-icons @@ -57,5 +61,9 @@ jobs: ${{ runner.os }}-pods- - name: Install dependencies run: flutter pub get + - name: Create .env files + run: | + echo "${{ secrets.ENV_PRODUCTION_FILE_CONTENT }}" > .env.production + echo "${{ secrets.ENV_DEVELOPMENT_FILE_CONTENT }}" > .env.development - name: Build iOS run: flutter build ios --release --no-codesign --no-tree-shake-icons diff --git a/.gitignore b/.gitignore index 8845805b..b92fa03e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ ios/fastlane/README.md ios/fastlane/.env.default ios/Runner.app.dSYM.zip ios/Runner.ipa + +.env +.env.* \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fdfdaae7..a49add29 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,5 @@ # 기여하기 - -## Release 절차(~ 2024.02.24 ) +## How to release( ~ 2024.02.24 ) 1. [Releases](https://github.com/sparcs-kaist/new-ara-app/releases)에 갑니다. 2. 이번 버전에서 변경된 사항을 Changes에 작성합니다.드래프트로 올립니다. @@ -9,14 +8,16 @@ 5. github release 페이지에서 드래프트를 풉니다. ~~버전을 하나 올린 후 `flutter build ios`를 반드시 실행해 주도록 합니다~~ + ios FastFile에 `sh "flutter build ios --release --no-codesign --no-tree-shake-icons"` 추가해서 fastlane시 항상 자동으로 빌드하도록 수정했습니다. -## Release 절차( 2024.02.25 ~) -릴리즈 노트를 자동으로 작성하는 cd 코드를 main 브랜치에 추가했습니다. +## How to release( 2024.02.25 ~ ) +릴리즈 노트를 자동으로 작성하는 cd 코드를 추가했습니다. 1. `pubspec.yaml`의 버전을 하나 올리는 커밋을 원격 저장소에 Pull Request하고 Merge 합니다. -2. fastlane으로 Android와 iOS에 deploy를 합니다. +2. 아래 **How to deploy** 항목을 보고 Android와 iOS에 deploy를 합니다. 3. [Releases](https://github.com/sparcs-kaist/new-ara-app/releases) 페이지에서 자동으로 등록된 릴리즈 노트의 내용을 수정합니다. +--- ## How to deploy ### Fastlane 설정 @@ -41,14 +42,14 @@ bundle install - `android/fastlane/newara-fastlane.json` : Google Play 서비스 계정 JSON 파일 - `android/fastlane/upload-keystore.jks` : Android App Signing Key for Upload Google Play - +
- `android/fastlane/.env` : 아래와 같이 각자 개인이 발급 받은 GITHUB_API_TOKEN을 추가합니다. ```env GITHUB_API_TOKEN=**************************************** ``` -GITHUB_API_TOKEN 발급 받는 법: https://lifefun.tistory.com/161 - +[GITHUB_API_TOKEN 발급 받는 법](https://docs.github.com/ko/enterprise-server@3.9/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) 상단 우측에 한국어 버젼 선택 가능 +
- `android/key.properties` : 아래와 같이 Signing Key 정보를 입력합니다. @@ -68,38 +69,67 @@ FASTLANE_PASSWORD=******** FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=****-****-****-**** GITHUB_API_TOKEN=**************************************** ``` +[APPLE application specific password(앱 암호) 발급 받는 법](https://support.apple.com/ko-kr/102654) + +
-- `ios/fastlane/Appfile` : 아래와 같이 Apple ID 계정 정보를 입력합니다. +- `ios/fastlane/Appfile` : 아래와 같이 Applefile이 되어있나 확인합니다. ```env app_identifier("org.sparcs.new-ara-app") # The bundle identifier of your app -apple_id("****@****.***") # Your Apple Developer Portal username +apple_id(ENV["FASTLANE_USER"]) # It automatically references .env.deault file itc_team_name("SPARCS") # App Store Connect Team Name team_id("N5V8W52U3U") # Developer Portal Team ID ``` -### 알파 버전 배포 +### 알파 버전 배포 명령어 - Android: Google Play 스토어 `비공개 테스트 - Alpha` 트랙으로 업로드 -- iOS: TestFlight로 업로드 +- iOS: `TestFlight`로 업로드 -아래 명령어 시 `pubspec.yaml`에 있는 버젼 정보와 현재 시각으로 지정된 빌드 정보 기준으로 업로드 됩니다. -**앱에서 백엔드 대상이 prod가 맞는지 배포 전 꼭 확인 하세요!** +**앱에서 배포 서버는 production을 기본으로 합니다** + +프로젝트 루트 디렉토리에서 아래 명령어 시 `pubspec.yaml`에 있는 버젼 정보와 현재 시각으로 지정된 빌드 정보 기준으로 업로드 됩니다. ```bash -cd android && bundle exec fastlane alpha && cd ../ios && bundle exec fastlane alpha +cd android && bundle exec fastlane alpha env:production && cd ../ios && bundle exec fastlane alpha env:production ``` +
아래 예시처럼 하나의 플랫폼에도 배포가 가능합니다. ```bash -cd ios && bundle exec fastlane alpha +cd ios && bundle exec fastlane alpha env:production +``` + +
+ +아래 예시처럼 dev 서버와 연결된 앱 배포도 가능합니다. + +```bash +cd ios && bundle exec fastlane alpha env:development +``` +
+ +아래 예시처럼 개별 fastlane 실행도 가능합니다. 아래는 깃허브 태그를 만드는 fastlane입니다. +```base +bundle exec fastlane create_release_note short_name:prod ``` ### 배포 후 작업 - `pubspec.yaml` 변경 사항을 Discard 합니다. - iOS Xcode 프로젝트 관련 파일들( `ios/Runner.xcodeproj/project.pbxproj`, `ios/Runner/Info.plist` )의 변경사항을 Discard 합니다. +## English translation +- 외부 패키지 사용했으니 자세한 내용은 [easy_localization](https://pub.dev/packages/easy_localization) 문서 읽어주시기 바랍니다. + +- `assets/translations`에 번역 파일을 json 형식으로 저장하고 있습니다. +- json 수정할 때마다 프로젝트의 root 디렉토리에서 **아래 명령어** 실행 후 dart 파일 내에서 `LocaleKeys.****` 같은 클래스 형식으로 가능합니다. +``` +dart run easy_localization:generate -S "assets/translations" -O "lib/translations" && dart run easy_localization:generate -S "assets/translations" -O "lib/translations" -f keys -o locale_keys.g.dart +``` + + diff --git a/README.md b/README.md index c436e9a2..a929a183 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@

Ara Logo

@@ -19,6 +19,7 @@ alt="Release version" /> +

+ +## How to Download +The SPARCS NewAra App is available for both Android and iOS. Follow the instructions below to download and install the app on your device. + +### Android +1. Visit the Google Play Store on your Android device. +2. Search for "Ara for KAIST". +3. Tap "Install" to download and install the app. + +[Play Store Link: Ara for KAIST](https://play.google.com/store/apps/details?id=org.sparcs.newara) + +### iOS +1. Open the App Store on your iOS device. +2. Search for "Ara for KAIST". +3. Tap "Get" to download and install the app. + +[App Store Link: Ara for KAIST](https://apps.apple.com/kr/app/ara-for-kaist/id6457209147) + +## How to develop +### run + +`FLUTTER_VERSION: "3.13"`, `JAVA_VERSION: "11"` + + +- `.env.development` : 프로젝트 루트 디렉토리에 `.env.development` 파일을 생성하고 아래와 같이 정보를 입력합니다. +```env +NEW_ARA_DEFAULT_URL=https://newara.dev.sparcs.org +NEW_ARA_AUTHORITY=newara.dev.sparcs.org +SPARCS_SSO_DEFAULT_URL=https://sparcssso.kaist.ac.kr +``` +
+ +- `.env.production` : 프로젝트 루트 디렉토리에 `.env.production` 파일을 생성하고 아래와 같이 정보를 입력합니다. +```env +NEW_ARA_DEFAULT_URL=https://newara.sparcs.org +NEW_ARA_AUTHORITY=newara.sparcs.org +SPARCS_SSO_DEFAULT_URL=https://sparcssso.kaist.ac.kr +``` + +
+ +#### Terminal + +- `development`와 `production` 둘 중 하나를 선택해서 실행합니다. +- 지정하지 않을 시 `development`로 자동 실행됩니다. + +ex) **`flutter run --dart-define=ENV=development`** + +ex) **`flutter run --dart-define=ENV=production`** + +#### VSCode + +[VSCode에서 디버깅 방법](https://code.visualstudio.com/docs/editor/debugging) + +- `launch.json`의 `configurations`에 아래 내용을 추가하면 `development`과 `production`을 전환하기 편합니다. +``` +{ + "version": "0.2.0", + "configurations": [ + { + "name": "new-ara-app(Delevopment)", + "request": "launch", + "type": "dart", + "args": [ + "--dart-define=ENV=development" + ] + }, + { + "name": "new-ara-app(Production)", + "request": "launch", + "type": "dart", + "args": [ + "--dart-define=ENV=production" + ], + }, + ] +} +``` + +## How to Deploy +- [CONTRIBUTING.md](https://github.com/sparcs-kaist/new-ara-app/blob/dev/CONTRIBUTING.md) 참조 diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..8d86db05 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,3 @@ + + #FFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index cc10614f..b3eb1941 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -2,7 +2,7 @@ - + + + diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 170def09..ca413ca8 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -27,7 +27,7 @@ platform :android do end desc "Create a release note on GitHub." - lane :create_release_note do + lane :create_release_note do |options| # YAML에서 버전 정보를 추출 @@ -46,9 +46,9 @@ platform :android do http_method: "POST", path: "/repos/sparcs-kaist/new-ara-app/releases", body: { - tag_name: "Android-v#{version_number}-#{version_code}", + tag_name: "Android-v#{version_number}-#{version_code}-#{options[:short_name]}", target_commitish: current_commit_hash, - name: "Android-v#{version_number}-#{version_code}", + name: "Android-v#{version_number}-#{version_code}-#{options[:short_name]}", body: "Write here the content of release notes...", draft: false, prerelease: false @@ -58,13 +58,30 @@ platform :android do end desc "Deploy a alpha version to the Google Play" - lane :alpha do + lane :alpha do |options| + unless ["production", "development"].include?(options[:env]) + UI.user_error!("Invalid env given, pass using `env: 'production'` or `env: 'development'`") + end + + short_name = "" + long_name = "" + + if(options[:env] == "production") + # 배포용 + short_name = "prod" + long_name = "production" + else + # 개발용 + short_name = "dev" + long_name = "development" + end + set_timestamp_version - sh "flutter build appbundle --no-tree-shake-icons" + sh "flutter build appbundle --no-tree-shake-icons --dart-define=ENV=#{long_name}" upload_to_play_store( aab: "../build/app/outputs/bundle/release/app-release.aab", track: "alpha", ) - create_release_note + create_release_note(short_name: short_name) end end diff --git a/assets/fonts/NotoSansKR-Black.otf b/assets/fonts/NotoSansKR-Black.otf deleted file mode 100644 index 5599581d..00000000 Binary files a/assets/fonts/NotoSansKR-Black.otf and /dev/null differ diff --git a/assets/fonts/NotoSansKR-Bold.otf b/assets/fonts/NotoSansKR-Bold.otf deleted file mode 100644 index be388bf5..00000000 Binary files a/assets/fonts/NotoSansKR-Bold.otf and /dev/null differ diff --git a/assets/fonts/NotoSansKR-Light.otf b/assets/fonts/NotoSansKR-Light.otf deleted file mode 100644 index 548e667e..00000000 Binary files a/assets/fonts/NotoSansKR-Light.otf and /dev/null differ diff --git a/assets/fonts/NotoSansKR-Medium.otf b/assets/fonts/NotoSansKR-Medium.otf deleted file mode 100644 index 5ddbbc03..00000000 Binary files a/assets/fonts/NotoSansKR-Medium.otf and /dev/null differ diff --git a/assets/fonts/NotoSansKR-Regular.otf b/assets/fonts/NotoSansKR-Regular.otf deleted file mode 100644 index 7c5c2fae..00000000 Binary files a/assets/fonts/NotoSansKR-Regular.otf and /dev/null differ diff --git a/assets/fonts/NotoSansKR-Thin.otf b/assets/fonts/NotoSansKR-Thin.otf deleted file mode 100644 index 1299fef0..00000000 Binary files a/assets/fonts/NotoSansKR-Thin.otf and /dev/null differ diff --git a/assets/fonts/Pretendard-Black.otf b/assets/fonts/Pretendard-Black.otf new file mode 100644 index 00000000..a0d849e7 Binary files /dev/null and b/assets/fonts/Pretendard-Black.otf differ diff --git a/assets/fonts/Pretendard-Bold.otf b/assets/fonts/Pretendard-Bold.otf new file mode 100644 index 00000000..8e5e30a2 Binary files /dev/null and b/assets/fonts/Pretendard-Bold.otf differ diff --git a/assets/fonts/Pretendard-ExtraBold.otf b/assets/fonts/Pretendard-ExtraBold.otf new file mode 100644 index 00000000..388f3ca4 Binary files /dev/null and b/assets/fonts/Pretendard-ExtraBold.otf differ diff --git a/assets/fonts/Pretendard-ExtraLight.otf b/assets/fonts/Pretendard-ExtraLight.otf new file mode 100644 index 00000000..40c8b69c Binary files /dev/null and b/assets/fonts/Pretendard-ExtraLight.otf differ diff --git a/assets/fonts/Pretendard-Light.otf b/assets/fonts/Pretendard-Light.otf new file mode 100644 index 00000000..228679e9 Binary files /dev/null and b/assets/fonts/Pretendard-Light.otf differ diff --git a/assets/fonts/Pretendard-Medium.otf b/assets/fonts/Pretendard-Medium.otf new file mode 100644 index 00000000..05750698 Binary files /dev/null and b/assets/fonts/Pretendard-Medium.otf differ diff --git a/assets/fonts/Pretendard-Regular.otf b/assets/fonts/Pretendard-Regular.otf new file mode 100644 index 00000000..08bf4cfc Binary files /dev/null and b/assets/fonts/Pretendard-Regular.otf differ diff --git a/assets/fonts/Pretendard-SemiBold.otf b/assets/fonts/Pretendard-SemiBold.otf new file mode 100644 index 00000000..e7e36abc Binary files /dev/null and b/assets/fonts/Pretendard-SemiBold.otf differ diff --git a/assets/fonts/Pretendard-Thin.otf b/assets/fonts/Pretendard-Thin.otf new file mode 100644 index 00000000..77e792d7 Binary files /dev/null and b/assets/fonts/Pretendard-Thin.otf differ diff --git a/assets/icons/comment.svg b/assets/icons/comment.svg index 1bf3ae64..e8f113a6 100644 --- a/assets/icons/comment.svg +++ b/assets/icons/comment.svg @@ -1,10 +1,3 @@ - - - - - - - - - + + diff --git a/assets/translations/en.json b/assets/translations/en.json index 6477fc45..9df135c2 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -1,36 +1,200 @@ { - "appBar": { - "chatting": "Chatting", - "notification": "Notifications", - "bulletin": "Bulletins" - }, - "main_page": { - "realtime": "Hot posts", - "notice": "Notices", - "stu_community": "Student Community" - }, - "myPage": { - "change": "change", - "mypost": "My Posts", - "scrap": "Scrapped", - "recent": "Recent" - }, - "login_page": { + "boardListPage": { + "boards": "Boards", + "searchBoardsPostsComments": "Search boards, posts, and comments", + "viewAll": "View All", + "topPosts": "Top Posts", + "bookmarks": "Bookmarks" + }, + "bulletinSearchPage": { + "searchIn": "Search in {en_name}", + "searchInAllPosts": "Search in all posts", + "searchInHistory": "Search in recently viewed posts", + "searchInTopPosts": "Search in real-time popular posts", + "searchInBookmarks": "Search in saved posts", + "search": "Search", + "pleaseEnter": "Please enter a search term", + "noResults": "No results found." + }, + "inquiryPage": { + "title": "Inquiries and Suggestions", + "reLoginErrorWithWithdrawalGuide": "This account has already been deactivated.\nIf you wish to re-register, please click this link to contact us via email.", + "reLoginErrorWithWithdrawalEmailTitle": "Re-registration Inquiry", + "reLoginErrorWithWithdrawalEmailContents": "If you have any additional comments, please write them below.\n\n※ An Ara administrator will respond within 48 hours.※\n\nUser ID: {userID}\nNickname: {nickname}\nEmail: {email}\nPlatform: App\n" + }, + "loginPage": { "login": "Sign in with SPARCS SSO" }, - "setting_page": { - "title": "setting", - "bulletin": "Bulletins", - "adult": "R-rated posts", - "politics": "Political posts", - "block": "Account Blocking", - "blocked_users": "List of Blocked Users", - "block_howto": "You can block accounts through the detail function in posts or comments.", - "noti": "Notifications", - "myreply": "Comments and Nested Comments of My Posts", - "reply": "Nested Comments", - "hot_noti": "Hot Notifications", - "hot_posts": "Hot Posts", - "hot_info": "We deliver hot announcements and posts every day at 8:30 a.m." + "mainPage": { + "topPost": "Top Posts", + "notice": "Notice", + "portalNotice": "Portal Notice", + "facility": "Facility", + "araAdmins": "Ara Admins", + "trades": "Trades", + "realEstate": "Real Estate", + "market": "Market", + "jobsWanted": "Jobs Wanted", + "organizationsAndClubs": "Organizations and Clubs", + "gradAssoc": "Grad Assoc", + "undergradAssoc": "Undergrad Assoc", + "freshmanCouncil": "Freshman Council", + "talk": "Talk" + }, + "notificationPage": { + "notifications": "Notifications", + "noNotifications": "No notifications.", + "today": "Today", + "post": "Post", + "newComment": "New comment to your post.", + "allNotificationsChecked": "You've already checked all the notifications." + }, + "postListShowPage": { + "boards": "Boards", + "history": "History", + "topPosts": "Top Posts", + "allPosts": "All Posts", + "bookmarks": "Bookmarks", + "testBoard": "Test Board" + }, + "settingPage": { + "title": "Setting", + "postSetting": "Post Setting", + "adult": "Allow Access to Adult Posts", + "politics": "Allow Access to Political Posts", + "block": "Blocked Users", + "viewBlockedUsers": "View Blocked Users", + "howToBlock": "You can block accounts through the detail function in posts or comments.", + "information": "Information", + "termsAndConditions": "Terms and Conditions of Service", + "contactAdmins": "Contact the Admins", + "signOut": "Sign out", + "withdrawal": "Withdrawal", + "withdrawalGuide": "Your request for account deletion will be processed after confirmation by the Ara administrator, and it may take up to 24 hours.", + "userBlockingGuide": "User blocking can be done through the 'More' option in posts. You can change it up to 10 times per day.", + "settingsSaved": "The settings have been saved.", + "errorSavingSettings": "An error occurred, and the settings could not be applied. Please try again.", + "myReplies": "Comments and Nested Comments of My Posts", + "replies": "Nested Comments", + "hotNotifications": "Hot Notifications", + "hotPosts": "Hot Posts", + "hotInfo": "We deliver trending announcements and posts every day at 8:30 a.m.", + "emailNotAvailable": "If the default mail application cannot be opened, please contact ara@sparcs.org." + }, + "userPage": { + "change": "Chg.", + "myPosts": "My Posts", + "bookmarks": "Bookmarks", + "history": "History", + "totalNPosts": "{curCount} posts", + "noEmailInfo": "No email information" + }, + "postViewPage": { + "reply": "Reply", + "scrap": "To Bookmark", + "scrapped": "Bookmarked", + "share": "Share", + "launchInBrowserNotAvailable": "The URL could not be opened by browser.", + "showHiddenPosts": "Show hidden posts", + "hit": "hits", + "noSelfVotingInfo": "You cannot vote for your post or comment!", + "failedToBlock": "Failed to block.", + "unblock": "Unblock", + "block": "Block", + "delete": "Delete", + "report": "Report", + "edit": "Edit", + "commentHintText": "Type your comment here.", + "noCommentWarning": "No comment has been written!", + "displayCommentCount": " comments", + "copyLinkToClipBoard": "Copied URL to the clipboard.", + "blockedUsersPost": "This post was written by blocked user.", + "blockedUsersComment": "This comment was written by blocked user.", + "reportedPost": "This post is hidden due to the accumulation of reports.", + "reportedComment": "This comment is hidden due to the accumulation of reports.", + "adultPost": "This post has adult/obscene contents.", + "socialPost": "This post has political/social contents.", + "accessDeniedPost": "Access for this post is denied.", + "hiddenPost": "This post is hidden.", + "deletedComment": "This comment is deleted.", + "hiddenComment": "This comment is hidden.", + "blockedUsersContentNotice": "You can change blocked users in your Setting page.", + "adultContentNotice": "You can change the setting in your Setting page to show this kinds of post.", + "socialContentNotice": "You can change the setting in your Setting page to show this kinds of post." + }, + "postViewUtils": { + "letUsKnowPostReportReason": "Let us know your reason for reporting the post.", + "letUsKnowCommentReportReason": "Let us know your reason for reporting the comment.", + "reportPostSucceed": "Post is successfully reported.", + "alreadyReported": "You've already reported this post.", + "reportButton": "Report", + "cancel": "Cancel" + }, + "dialogs": { + "deleteConfirm": "Do you really want to delete this post?", + "blockConfirm": "Do you really want to block this user?", + "logoutConfirm": "Do you really want to sign out?", + "withdrawalConfirm": "Do you really want to withdraw the membership?", + "withdrawalEmailInfo": "If you leave the membership, you can't re-sign up with the email you're using now", + "cancel": "Cancel", + "confirm": "OK", + "noBlockedUsers": "There are no blocked users.", + "noNickname": "No nickname" + }, + "popUpMenuButtons": { + "downloadSucceed": "File downloaded successfully", + "downloadFailed": "Downloading File failed", + "attachments": "Attachments", + "report": "Report", + "edit": "Edit", + "delete": "Delete", + "withSchoolInfoText": "This board is for students to freely share their opinions with the school. Any post with over 20 votes will get an official reply from the school. Please be aware that all posts here are made with real names, for clear and responsible communication. " + }, + "postPreview": { + "blockedUsersPost": "This post was written by blocked user.", + "reportedPost": "This post is hidden due to the accumulation of reports.", + "adultPost": "This post has adult/obscene contents.", + "socialPost": "This post has political/social contents.", + "accessDeniedPost": "Access for this post is denied.", + "hiddenPost": "This post is hidden.", + "beforeUpVoteThreshold": "Polling", + "beforeSchoolConfirm": "Preparing", + "answerDone": "Answered" + }, + "profileEditPage": { + "settingInfoText": "There was a problem changing the settings.", + "editProfile": "Edit Profile", + "complete": "Complete", + "nickname": "Nickname", + "email": "Email", + "noEmail": "Email doesn't exist", + "nicknameInfo": "Once you change your nickname, you can't change it for three months.", + "nicknameHintText": "Please enter the nickname you want to change to.", + "nicknameEmptyInfo": "Nickname not provided!" + }, + "postWritePage": { + "write": "Write a post", + "submit": "Submit", + "titleHintText": "Type title here", + "selectBoard": "Select Board", + "addAttach": "Upload Attachments", + "attachments": "Attachments", + "terms": "Terms", + "realNameNotice": "This post will be under your real name.", + "anonymous": "Anon", + "adult": "Adult", + "politics": "Politics", + "contentPlaceholder": "Type content here", + "conditionSnackBar": "Please select a board and enter the title and content.", + "noCategory": "No Topics", + "selectCategory": "Select Topic" + }, + "termsAndConditionsPage": { + "termsAndConditions": "Terms and Conditions", + "agree": "Agree", + "agreed": "You've already agreed." + }, + "userProvider" :{ + "internetError" : "Please check your network." } } \ No newline at end of file diff --git a/assets/translations/ko.json b/assets/translations/ko.json index fcff48d0..3b66d591 100644 --- a/assets/translations/ko.json +++ b/assets/translations/ko.json @@ -1,36 +1,200 @@ { - "appBar": { - "chatting": "채팅", - "notification": "알림", - "bulletin": "게시판" + "boardListPage": { + "boards": "게시판", + "searchBoardsPostsComments": "게시판, 게시글 및 댓글 검색", + "viewAll": "전체보기", + "topPosts": "인기글", + "bookmarks": "담아둔 글" }, - "main_page": { - "realtime": "실시간 인기글", - "notice": "공지", - "stu_community": "학생 단체" + "bulletinSearchPage": { + "searchIn": "{ko_name}에서 검색", + "searchInAllPosts": "전체 보기에서 검색", + "searchInHistory": "최근 본 글에서 검색", + "searchInTopPosts": "실시간 인기글에서 검색", + "searchInBookmarks": "담아둔 글에서 검색", + "pleaseEnter": "검색어를 입력하세요", + "search": "검색", + "noResults": "검색 결과가 없습니다." }, - "myPage": { - "change": "수정", - "mypost": "작성한 글", - "scrap": "담아둔 글", - "recent": "최근 본 글" + "inquiryPage": { + "title": "문의 및 건의", + "reLoginErrorWithWithdrawalGuide": "이미 탈퇴 했던 계정입니다.\n재가입을 하고 싶다면 이 링크를 눌러 이메일로 문의하세요.", + "reLoginErrorWithWithdrawalEmailTitle": "재가입 문의", + "reLoginErrorWithWithdrawalEmailContents": "추가로 말하실 말이 있다면 여기 아래에 적어주세요.\n\n※ Ara 관리자가 48시간 이내로 답변드립니다.※\n\n유저 번호: {userID}\n닉네임: {nickname}\n이메일: {email}\n플랫폼: App\n" }, - "login_page" : { + "loginPage": { "login": "SPARCS SSO로 로그인" }, - "setting_page": { + "mainPage": { + "topPost": "실시간 인기글", + "notice": "공지", + "portalNotice": "포탈 공지", + "facility": "입주 업체", + "araAdmins": "Ara 운영진", + "trades": "거래", + "realEstate": "부동산", + "market": "중고거래", + "jobsWanted": "구인구직", + "organizationsAndClubs": "학생 단체", + "gradAssoc": "원총", + "undergradAssoc": "총학", + "freshmanCouncil": "새학", + "talk": "자유게시판" + }, + "notificationPage": { + "notifications": "알림", + "noNotifications": "알림이 없습니다.", + "today": "오늘", + "post": "게시물", + "newComment": "회원님의 게시물에 새로운 댓글이 작성되었습니다.", + "allNotificationsChecked": "이미 알림을 모두 읽으셨습니다." + }, + "postListShowPage": { + "boards": "게시판", + "history": "최근 본 글", + "topPosts": "실시간 인기글", + "allPosts": "전체보기", + "bookmarks": "담아둔 글", + "testBoard": "테스트 게시판" + }, + "settingPage": { "title": "설정", - "bulletin": "게시글", + "postSetting": "게시글 설정", "adult": "성인글 보기", "politics": "정치글 보기", "block": "차단", - "blocked_users": "차단한 유저 목록", - "block_howto": "유저 차단은 게시글이나 댓글에서 더보기 기능을 통해 가능합니다.", - "noti": "알림", - "myreply": "내 글에 달린 댓글 및 대댓글", - "reply": "댓글에 달린 대댓글", - "hot_noti": "인기 공지글", - "hot_posts": "인기글", - "hot_info": "인기 공지글 및 인기 글을 매일 오전 8시 30분에 전달해 드립니다." + "viewBlockedUsers": "차단한 유저 목록", + "howToBlock": "유저 차단은 게시글이나 댓글에서 더보기 기능을 통해 가능합니다.", + "information": "정보", + "termsAndConditions": "이용약관", + "contactAdmins": "운영진에게 문의하기", + "signOut": "로그아웃", + "withdrawal": "회원탈퇴", + "withdrawalGuide": "회원 탈퇴는 Ara 관리자가 확인 후 처리해드리며, 최대 24시간이 소요될 수 있습니다.", + "userBlockingGuide": "유저 차단은 게시글의 '더보기' 기능에서 하실 수 있습니다. 하루에 최대 10번만 변경 가능합니다.", + "settingsSaved": "설정이 저장되었습니다.", + "errorSavingSettings": "에러가 발생하여 설정 반영에 실패했습니다. 다시 시도해주십시오.", + "myReplies": "내 글에 달린 댓글 및 대댓글", + "replies": "댓글에 달린 대댓글", + "hotNotifications": "인기 공지글", + "hotPosts": "인기글", + "hotInfo": "인기 공지글 및 인기 글을 매일 오전 8시 30분에 전달해 드립니다.", + "emailNotAvailable": "기본 메일 어플리케이션을 열 수 없습니다. ara@sparcs.org로 문의 부탁드립니다." + }, + "userPage": { + "change": "수정", + "myPosts": "작성한 글", + "bookmarks": "담아둔 글", + "history": "최근 본 글", + "totalNPosts": "총 {curCount}개의 글", + "noEmailInfo": "이메일 정보가 없습니다." + }, + "postViewPage": { + "reply": "답글 쓰기", + "scrap": "담아두기", + "scrapped": "담아둔 글", + "share": "공유", + "launchInBrowserNotAvailable": "브라우저로 URL을 열 수 없습니다.", + "showHiddenPosts": "숨긴내용 보기", + "hit": "조회", + "noSelfVotingInfo": "본인 게시글이나 댓글에는 좋아요를 누를 수 없습니다.", + "failedToBlock": "차단에 실패했습니다.", + "unblock": "차단 해제", + "block": "차단", + "delete": "삭제", + "report": "신고", + "edit": "수정", + "commentHintText": "댓글을 입력해주세요.", + "noCommentWarning": "댓글이 작성되지 않았습니다!", + "displayCommentCount": "개의 댓글", + "copyLinkToClipBoard": "URL을 클립 보드에 복사했습니다.", + "blockedUsersPost": "차단한 사용자의 게시물입니다.", + "blockedUsersComment": "차단한 사용자의 댓글입니다.", + "reportedPost": "신고 누적으로 숨김된 게시물입니다.", + "reportedComment": "신고 누적으로 숨김된 댓글입니다.", + "adultPost": "성인/음란성 내용의 게시물입니다.", + "socialPost": "정치/사회성 내용의 게시물입니다.", + "accessDeniedPost": "접근 권한이 없는 게시물입니다.", + "hiddenPost": "숨겨진 게시물입니다.", + "deletedComment": "삭제된 댓글입니다.", + "hiddenComment": "숨겨진 댓글입니다.", + "blockedUsersContentNotice": "차단 사용자 설정은 설정페이지에서 수정할 수 있습니다.", + "adultContentNotice": "게시글 보기 설정은 설정페이지에서 수정할 수 있습니다.", + "socialContentNotice": "게시글 보기 설정은 설정페이지에서 수정할 수 있습니다." + }, + "postViewUtils": { + "letUsKnowPostReportReason": "게시글 신고 사유를 알려주세요.", + "letUsKnowCommentReportReason": "댓글 신고 사유를 알려주세요.", + "reportPostSucceed": "해당 게시글을 신고했습니다.", + "alreadyReported": "이미 신고한 게시글입니다.", + "reportButton": "신고하기", + "cancel": "취소" + }, + "dialogs": { + "deleteConfirm": "정말로 삭제하시겠습니까?", + "blockConfirm": "정말로 차단하시겠습니까?", + "logoutConfirm": "정말로 로그아웃 하시겠습니까?", + "withdrawalConfirm": "정말로 회원탈퇴 하시겠습니까?", + "withdrawalEmailInfo": "회원탈퇴하시면 지금 쓰시는 이메일로는 재가입이 불가능합니다", + "cancel": "취소", + "confirm": "확인", + "noBlockedUsers": "차단한 유저가 없습니다.", + "noNickname": "닉네임이 없음" + }, + "popUpMenuButtons": { + "downloadSucceed": "파일 다운로드에 성공했습니다", + "downloadFailed": "파일 다운로드에 실패했습니다", + "attachments": "첨부파일 모아보기", + "report": "신고", + "edit": "수정", + "delete": "삭제", + "withSchoolInfoText": "본 게시판은 교내 구성원들이 실명으로 학교에 의견을 제시하는 게시판이며, 좋아요 수가 20개 이상인 모든 글에 대해 학교 측 공식 답변을 받으실 수 있습니다. 투명하고 책임감 있는 의견 공유를 위해 댓글 작성 시 실명으로 공개됩니다." + }, + "postPreview": { + "blockedUsersPost": "차단한 사용자의 게시물입니다.", + "reportedPost": "신고 누적으로 숨김된 게시물입니다.", + "adultPost": "성인/음란성 내용의 게시물입니다.", + "socialPost": "정치/사회성 내용의 게시물입니다.", + "accessDeniedPost": "접근 권한이 없는 게시물입니다.", + "hiddenPost": "숨겨진 게시물입니다.", + "beforeUpVoteThreshold": "달성 전", + "beforeSchoolConfirm": "답변 대기 중", + "answerDone": "답변 완료" + }, + "profileEditPage": { + "settingInfoText": "설정 변경 중 문제가 발생했습니다.", + "editProfile": "프로필 수정", + "complete": "완료", + "nickname": "닉네임", + "email": "이메일", + "noEmail": "이메일 정보가 없습니다.", + "nicknameInfo": "닉네임은 한번 변경할 시 3개월간 변경이 불가합니다.", + "nicknameHintText": "변경하실 닉네임을 입력해주세요.", + "nicknameEmptyInfo": "닉네임이 작성되지 않았습니다!" + }, + "postWritePage": { + "write": "글 쓰기", + "submit": "올리기", + "titleHintText": "제목을 입력해주세요.", + "selectBoard": "게시판을 선택하세요", + "addAttach": "첨부파일 추가", + "attachments": "첨부파일", + "terms": "이용약관", + "realNameNotice": "이 게시물은 실명으로 게시됩니다.", + "anonymous": "익명", + "adult": "성인", + "politics": "정치", + "contentPlaceholder": "내용을 입력해주세요.", + "conditionSnackBar": "게시판을 선택해주시고 제목, 내용을 입력해주세요.", + "noCategory": "말머리 없음", + "selectCategory": "말머리를 선택하세요" + }, + "termsAndConditionsPage": { + "termsAndConditions": "이용약관", + "agree": "동의 하기", + "agreed": "이미 동의하셨습니다." + }, + "userProvider" :{ + "internetError" : "인터넷 오류가 발생하였습니다." } } \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5e13d96a..cb3e2bc8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -170,8 +170,8 @@ SPEC CHECKSUMS: url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 video_player_avfoundation: 8563f13d8fc8b2c29dc2d09e60b660e4e8128837 webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 - webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a + webview_flutter_wkwebview: 4f3e50f7273d31e5500066ed267e3ae4309c5ae4 PODFILE CHECKSUM: 40e3ca182cfbd571c46e751b6eb8a6f2cbbf72fc -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.2 diff --git a/ios/fastlane/Appfile b/ios/fastlane/Appfile index a75b5273..bb1d925c 100644 --- a/ios/fastlane/Appfile +++ b/ios/fastlane/Appfile @@ -1,5 +1,5 @@ app_identifier("org.sparcs.new-ara-app") # The bundle identifier of your app -apple_id("tkddh1109@gmail.com") # Your Apple Developer Portal username +apple_id(ENV["FASTLANE_USER"]) # It automatically references .env.deault file itc_team_name("SPARCS") # App Store Connect Team Name team_id("N5V8W52U3U") # Developer Portal Team ID diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 7361f1f6..86e37e19 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,7 +18,7 @@ default_platform(:ios) platform :ios do desc "Create a release note on GitHub." - lane :create_release_note do + lane :create_release_note do |options| # YAML 파일에서 버전 정보를 로드 yaml_file_path = "../../pubspec.yaml" data = YAML.load_file(yaml_file_path) @@ -36,9 +36,9 @@ platform :ios do http_method: "POST", path: "/repos/sparcs-kaist/new-ara-app/releases", body: { - tag_name: "iOS-v#{version_number}-#{version_code}", + tag_name: "iOS-v#{version_number}-#{version_code}-#{options[:short_name]}", target_commitish: current_commit_hash, - name: "iOS-v#{version_number}-#{version_code}", + name: "iOS-v#{version_number}-#{version_code}-#{options[:short_name]}", body: "Write here the content of release notes...", draft: false, prerelease: false @@ -48,7 +48,24 @@ platform :ios do end desc "Push a new beta build to TestFlight" - lane :alpha do + lane :alpha do |options| + unless ["production", "development"].include?(options[:env]) + UI.user_error!("Invalid env given, pass using `env: 'production'` or `env: 'development'`") + end + + short_name = "" + long_name = "" + + if(options[:env] == "production") + # 배포용 + short_name = "prod" + long_name = "production" + else + # 개발용 + short_name = "dev" + long_name = "development" + end + increment_build_number( build_number: Time.now.strftime("%y%m%d.%H%M"), xcodeproj: "Runner.xcodeproj", @@ -61,7 +78,7 @@ platform :ios do ) #버젼 변경할 때마다 빌드해야하는 번거로움을 줄이기 위해서 코드 추가 - sh "flutter build ios --release --no-codesign --no-tree-shake-icons" + sh "flutter build ios --release --no-codesign --no-tree-shake-icons --dart-define=ENV=#{long_name}" build_app( workspace: "Runner.xcworkspace", @@ -72,6 +89,6 @@ platform :ios do upload_to_testflight( skip_waiting_for_build_processing: true, ) - create_release_note + create_release_note(short_name: short_name) end end diff --git a/lib/constants/url_info.dart b/lib/constants/url_info.dart index 8f5cd8aa..7d16e67a 100644 --- a/lib/constants/url_info.dart +++ b/lib/constants/url_info.dart @@ -1,4 +1,3 @@ -//const newAraDefaultUrl = "https://newara.dev.sparcs.org"; -const newAraDefaultUrl = "https://newara.sparcs.org"; -const newAraAuthority = "newara.sparcs.org"; -const sparcsSSODefaultUrl = "https://sparcssso.kaist.ac.kr"; +late final String newAraDefaultUrl; +late final String newAraAuthority; +late final String sparcsSSODefaultUrl; diff --git a/lib/main.dart b/lib/main.dart index d4819155..fe6d7d31 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:new_ara_app/pages/terms_and_conditions_page.dart'; +import 'package:new_ara_app/constants/url_info.dart'; +import 'package:new_ara_app/translations/codegen_loader.g.dart'; +import 'package:new_ara_app/utils/global_key.dart'; import 'package:new_ara_app/widgets/loading_indicator.dart'; import 'package:provider/provider.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; - import 'package:new_ara_app/pages/main_navigation_tab_page.dart'; import 'package:new_ara_app/pages/login_page.dart'; import 'package:new_ara_app/providers/user_provider.dart'; @@ -23,6 +25,17 @@ void main() async { FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); await EasyLocalization.ensureInitialized(); + // .env.delevopment 또는 .env.production 파일을 읽어서 환경변수 설정 + // ex) flutter run --dart-define=ENV=development + // ex) flutter run --dart-define=ENV=production + const String environment = + String.fromEnvironment('ENV', defaultValue: 'development'); + await dotenv.load(fileName: ".env.$environment"); + + newAraDefaultUrl = dotenv.env['NEW_ARA_DEFAULT_URL']!; + newAraAuthority = dotenv.env['NEW_ARA_AUTHORITY']!; + sparcsSSODefaultUrl = dotenv.env['SPARCS_SSO_DEFAULT_URL']!; + // 앱 시작점. 다국어 지원 및 여러 데이터 제공자를 포함한 구조로 설정 runApp( EasyLocalization( @@ -30,6 +43,7 @@ void main() async { path: 'assets/translations', fallbackLocale: const Locale('en'), startLocale: const Locale('ko'), + assetLoader: const CodegenLoader(), child: MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => UserProvider()), @@ -71,6 +85,7 @@ class _MyAppState extends State { @override void initState() { super.initState(); + // 자동 로그인을 위한 초기 설정 autoLoginByGetCookie(Provider.of(context, listen: false)); @@ -113,6 +128,7 @@ class _MyAppState extends State { localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, + scaffoldMessengerKey: snackBarKey, // theme: _setThemeData(), // TODO: CustionScrollBehavior의 역할은? builder: (context, child) { @@ -144,7 +160,7 @@ class _MyAppState extends State { return ThemeData( appBarTheme: const AppBarTheme(elevation: 0, backgroundColor: Colors.white), - fontFamily: 'NotoSansKR', + fontFamily: 'Pretendard', scaffoldBackgroundColor: Colors.white, splashColor: Colors.transparent, textSelectionTheme: const TextSelectionThemeData( diff --git a/lib/models/article_list_action_model.dart b/lib/models/article_list_action_model.dart index 12a69650..0b1853c5 100644 --- a/lib/models/article_list_action_model.dart +++ b/lib/models/article_list_action_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:new_ara_app/models/public_user_model.dart'; import 'package:new_ara_app/models/topic_model.dart'; import 'package:new_ara_app/models/board_model.dart'; diff --git a/lib/models/article_model.dart b/lib/models/article_model.dart index b3264b8e..21ce7a9a 100644 --- a/lib/models/article_model.dart +++ b/lib/models/article_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:flutter/material.dart'; import 'package:new_ara_app/models/topic_model.dart'; @@ -25,7 +27,7 @@ class ArticleModel { PublicUserModel created_by; int? article_current_page; dynamic side_articles; // 변수의 타입 및 용도 불분명 - dynamic communication_article_status; // 타입 및 용도 불분명 + dynamic communication_article_status; // 학교에게 전합니다 게시글에 적용 (0: 달성 전, 1: 답변 대기 중, 2: 답변 완료) dynamic days_left; // 타입 불분명 String created_at; String updated_at; diff --git a/lib/models/article_nested_comment_list_action_model.dart b/lib/models/article_nested_comment_list_action_model.dart index 5abb4acf..9a332acf 100644 --- a/lib/models/article_nested_comment_list_action_model.dart +++ b/lib/models/article_nested_comment_list_action_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:flutter/material.dart'; import 'package:new_ara_app/models/public_user_model.dart'; import 'package:new_ara_app/models/comment_nested_comment_list_action_model.dart'; diff --git a/lib/models/article_page_model.dart b/lib/models/article_page_model.dart index e3fc0a14..fba9b774 100644 --- a/lib/models/article_page_model.dart +++ b/lib/models/article_page_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:new_ara_app/models/article_list_action_model.dart'; class ArticlePageModel { diff --git a/lib/models/attachment_model.dart b/lib/models/attachment_model.dart index a161ccf9..d4def61e 100644 --- a/lib/models/attachment_model.dart +++ b/lib/models/attachment_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + class AttachmentModel { int id; String created_at; diff --git a/lib/models/base_article_model.dart b/lib/models/base_article_model.dart index 18b2654b..370b55d0 100644 --- a/lib/models/base_article_model.dart +++ b/lib/models/base_article_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + class BaseArticleModel { int id; String created_at; diff --git a/lib/models/board_detail_action_model.dart b/lib/models/board_detail_action_model.dart index 2d760ae2..12fb3109 100644 --- a/lib/models/board_detail_action_model.dart +++ b/lib/models/board_detail_action_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:new_ara_app/models/topic_model.dart'; import 'package:new_ara_app/models/simple_board_model.dart'; diff --git a/lib/models/board_group_model.dart b/lib/models/board_group_model.dart index 03591ade..48471249 100644 --- a/lib/models/board_group_model.dart +++ b/lib/models/board_group_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:new_ara_app/models/simple_board_model.dart'; class BoardGroupModel { diff --git a/lib/models/board_model.dart b/lib/models/board_model.dart index 35b2b14f..b886de9c 100644 --- a/lib/models/board_model.dart +++ b/lib/models/board_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + class BoardModel { final int id; final String slug; diff --git a/lib/models/comment_model.dart b/lib/models/comment_model.dart index 42524c7d..6a62b0e7 100644 --- a/lib/models/comment_model.dart +++ b/lib/models/comment_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:new_ara_app/models/public_user_model.dart'; class CommentModel { diff --git a/lib/models/comment_nested_comment_list_action_model.dart b/lib/models/comment_nested_comment_list_action_model.dart index 4be09b15..7114cb91 100644 --- a/lib/models/comment_nested_comment_list_action_model.dart +++ b/lib/models/comment_nested_comment_list_action_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:new_ara_app/models/public_user_model.dart'; class CommentNestedCommentListActionModel { diff --git a/lib/models/notification_model.dart b/lib/models/notification_model.dart index d4eae56e..3147c864 100644 --- a/lib/models/notification_model.dart +++ b/lib/models/notification_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:new_ara_app/models/base_article_model.dart'; class NotificationModel { diff --git a/lib/models/public_user_model.dart b/lib/models/public_user_model.dart index 67cfe960..85afb351 100644 --- a/lib/models/public_user_model.dart +++ b/lib/models/public_user_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:new_ara_app/models/public_user_profile_model.dart'; class PublicUserModel { diff --git a/lib/models/public_user_profile_model.dart b/lib/models/public_user_profile_model.dart index a377a4dd..616c93b0 100644 --- a/lib/models/public_user_profile_model.dart +++ b/lib/models/public_user_profile_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + class PublicUserProfileModel { String? picture; String? nickname; diff --git a/lib/models/scrap_create_action_model.dart b/lib/models/scrap_create_action_model.dart index f64477ea..6e51a55f 100644 --- a/lib/models/scrap_create_action_model.dart +++ b/lib/models/scrap_create_action_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + class ScrapCreateActionModel { int id; String created_at; diff --git a/lib/models/scrap_model.dart b/lib/models/scrap_model.dart index a59a6484..10698106 100644 --- a/lib/models/scrap_model.dart +++ b/lib/models/scrap_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + import 'package:new_ara_app/models/public_user_model.dart'; import 'package:new_ara_app/models/article_list_action_model.dart'; @@ -19,13 +21,13 @@ class ScrapModel { }); ScrapModel.fromJson(Map json) - : this.id = json['id'], - this.parent_article = + : id = json['id'], + parent_article = ArticleListActionModel.fromJson(json['parent_article']), - this.scrapped_by = PublicUserModel.fromJson(json['scrapped_by']), - this.created_at = json['created_at'], - this.updated_at = json['updated_at'], - this.deleted_at = json['deleted_at']; + scrapped_by = PublicUserModel.fromJson(json['scrapped_by']), + created_at = json['created_at'], + updated_at = json['updated_at'], + deleted_at = json['deleted_at']; Map toJson() => { 'id': id, diff --git a/lib/models/simple_board_model.dart b/lib/models/simple_board_model.dart index 0a6f8110..24a82370 100644 --- a/lib/models/simple_board_model.dart +++ b/lib/models/simple_board_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + class SimpleBoardModel { int id; String slug; diff --git a/lib/models/topic_model.dart b/lib/models/topic_model.dart index 6f656c1d..dcfea7ec 100644 --- a/lib/models/topic_model.dart +++ b/lib/models/topic_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + class TopicModel { final int id; final String slug; diff --git a/lib/models/user_profile_model.dart b/lib/models/user_profile_model.dart index f2c621e3..449f3eda 100644 --- a/lib/models/user_profile_model.dart +++ b/lib/models/user_profile_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: non_constant_identifier_names + class UserProfileModel { final int user; final String? email; diff --git a/lib/pages/bulletin_list_page.dart b/lib/pages/board_list_page.dart similarity index 90% rename from lib/pages/bulletin_list_page.dart rename to lib/pages/board_list_page.dart index 1f94774e..b7be69b3 100644 --- a/lib/pages/bulletin_list_page.dart +++ b/lib/pages/board_list_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:new_ara_app/models/board_group_model.dart'; import 'package:new_ara_app/pages/bulletin_search_page.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/utils/cache_function.dart'; import 'package:provider/provider.dart'; @@ -17,25 +19,24 @@ import 'package:new_ara_app/providers/notification_provider.dart'; const boardsByGroupLength = 5; /// `BulletinListPage`는 사용자가 이 페이지에서 다양한 게시판을 탐색하고 선택함.. -class BulletinListPage extends StatefulWidget { - const BulletinListPage({Key? key}) : super(key: key); +class BoardListPage extends StatefulWidget { + const BoardListPage({super.key}); @override - State createState() => _BulletinListPageState(); + State createState() => _BoardListPageState(); } -class _BulletinListPageState extends State { +class _BoardListPageState extends State { /// 데이터 로딩 상태 bool isLoading = true; /// 게시판 그룹 별로 게시판 목록 저장하는 변수. 게시판 그룹은 1부터 시작하도록 초기화. List> boardsByGroup = List.generate(boardsByGroupLength + 1, (_) => []); - List> textContent = []; + List boardModels = []; final FocusNode _focusNode = FocusNode(); @override void initState() { - // TODO: implement initState super.initState(); var userProvider = Provider.of(context, listen: false); refreshBoardList(userProvider); @@ -82,7 +83,7 @@ class _BulletinListPageState extends State { appBar: AppBar( centerTitle: false, title: Text( - "appBar.bulletin".tr(), + LocaleKeys.boardListPage_boards.tr(), style: const TextStyle( fontSize: 28, fontWeight: FontWeight.w700, @@ -148,11 +149,12 @@ class _BulletinListPageState extends State { ), ), ), - hintText: '게시판, 게시글 및 댓글 검색', + hintText: LocaleKeys + .boardListPage_searchBoardsPostsComments + .tr(), hintStyle: const TextStyle( color: Color(0xFFBBBBBB), fontSize: 16, - fontFamily: 'NotoSansKR', height: null, fontWeight: FontWeight.w500, ), @@ -203,9 +205,9 @@ class _BulletinListPageState extends State { const SizedBox( width: 5, ), - const Text( - '전체보기', - style: TextStyle( + Text( + LocaleKeys.boardListPage_viewAll.tr(), + style: const TextStyle( color: Color(0xFF333333), fontSize: 17, fontWeight: FontWeight.w700, @@ -244,9 +246,9 @@ class _BulletinListPageState extends State { const SizedBox( width: 5, ), - const Text( - '인기글', - style: TextStyle( + Text( + LocaleKeys.boardListPage_topPosts.tr(), + style: const TextStyle( color: Color(0xFF333333), fontSize: 17, fontWeight: FontWeight.w700, @@ -285,9 +287,9 @@ class _BulletinListPageState extends State { const SizedBox( width: 5, ), - const Text( - '담아둔 글', - style: TextStyle( + Text( + LocaleKeys.boardListPage_bookmarks.tr(), + style: const TextStyle( color: Color(0xFF333333), fontSize: 17, fontWeight: FontWeight.w700, @@ -308,7 +310,7 @@ class _BulletinListPageState extends State { height: 10, ), - BoardExpansionTile(1, "공지", boardsByGroup[1]), + BoardExpansionTile(1, boardsByGroup[1]), /// 자유 게시판은 별도의 하위 목록이 없기에 따로 처리 InkWell( @@ -337,20 +339,21 @@ class _BulletinListPageState extends State { width: 5, ), Text( - boardsByGroup[2][0].ko_name, + context.locale == const Locale("ko") + ? boardsByGroup[2][0].ko_name + : boardsByGroup[2][0].en_name, style: const TextStyle( color: Color(0xFF333333), fontSize: 20, - fontFamily: 'NotoSansKR', fontWeight: FontWeight.w700, ), ), ], ), )), - BoardExpansionTile(3, "학생 단체 및 동아리", boardsByGroup[3]), - BoardExpansionTile(4, "거래", boardsByGroup[4]), - BoardExpansionTile(5, "소통", boardsByGroup[5]), + BoardExpansionTile(3, boardsByGroup[3]), + BoardExpansionTile(4, boardsByGroup[4]), + BoardExpansionTile(5, boardsByGroup[5]), ], ), ), @@ -368,13 +371,11 @@ class _BulletinListPageState extends State { class BoardExpansionTile extends StatelessWidget { // TODO: titleNum이 필요한 이유 알기 final int titleNum; - final String title; final List boardsByGroup; /// [titleNum]은 게시판 그룹의 번호, [title]은 게시판 그룹의 제목, /// [boardsByGroup]은 해당 그룹에 속한 게시판 목록. - const BoardExpansionTile(this.titleNum, this.title, this.boardsByGroup, - {super.key}); + const BoardExpansionTile(this.titleNum, this.boardsByGroup, {super.key}); @override Widget build(BuildContext context) { @@ -409,7 +410,9 @@ class BoardExpansionTile extends StatelessWidget { width: 5, ), Text( - title, + context.locale == const Locale("ko") + ? boardsByGroup[0].group.ko_name + : boardsByGroup[0].group.en_name, style: const TextStyle( color: Color(0xFF333333), fontSize: 20, @@ -433,7 +436,9 @@ class BoardExpansionTile extends StatelessWidget { width: 40, ), Text( - model.ko_name, + context.locale == const Locale("ko") + ? model.ko_name + : model.en_name, style: const TextStyle( color: Color(0xFF333333), fontSize: 16, diff --git a/lib/pages/bulletin_search_page.dart b/lib/pages/bulletin_search_page.dart index 9fb7e956..4d5ee908 100644 --- a/lib/pages/bulletin_search_page.dart +++ b/lib/pages/bulletin_search_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:new_ara_app/constants/board_type.dart'; @@ -8,6 +9,7 @@ import 'package:new_ara_app/models/article_list_action_model.dart'; import 'package:new_ara_app/models/board_detail_action_model.dart'; import 'package:new_ara_app/pages/post_view_page.dart'; import 'package:new_ara_app/providers/user_provider.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/utils/slide_routing.dart'; import 'package:new_ara_app/widgets/loading_indicator.dart'; import 'package:new_ara_app/widgets/post_preview.dart'; @@ -39,7 +41,6 @@ class _BulletinSearchPageState extends State { @override void initState() { - // TODO: implement initState super.initState(); // 게시판 유형에 따라 API URL 및 힌트 텍스트를 다르게 설정 @@ -48,36 +49,37 @@ class _BulletinSearchPageState extends State { case BoardType.free: _apiUrl = "articles/?parent_board=${widget.boardInfo!.id.toInt()}&page="; - _hintText = "${widget.boardInfo!.ko_name}에서 검색"; + _hintText = LocaleKeys.bulletinSearchPage_searchIn.tr(namedArgs: { + "en_name": widget.boardInfo!.en_name, + "ko_name": widget.boardInfo!.ko_name + }); break; case BoardType.all: _apiUrl = "articles/?page="; - _hintText = "전체 보기에서 검색"; + _hintText = LocaleKeys.bulletinSearchPage_searchInAllPosts.tr(); break; case BoardType.recent: _apiUrl = "articles/recent/?page="; - _hintText = "최근 본 글에서 검색"; + _hintText = LocaleKeys.bulletinSearchPage_searchInHistory.tr(); break; case BoardType.top: _apiUrl = "articles/top/?page="; - _hintText = "실시간 인기글에서 검색"; + _hintText = LocaleKeys.bulletinSearchPage_searchInTopPosts.tr(); break; case BoardType.scraps: _apiUrl = "scraps/?page="; - _hintText = "담아둔 글에서 검색"; + _hintText = LocaleKeys.bulletinSearchPage_searchInBookmarks.tr(); break; default: _apiUrl = "articles/recent/?page="; - _hintText = "검색"; + _hintText = LocaleKeys.bulletinSearchPage_search.tr(); break; } - - UserProvider userProvider = context.read(); // 위젯이 빌드된 후에 포커스를 줍니다. WidgetsBinding.instance .addPostFrameCallback((_) => _focusNode.requestFocus()); _scrollController.addListener(_scrollListener); - refreshPostList(""); + _initPostList(""); } @override @@ -89,8 +91,8 @@ class _BulletinSearchPageState extends State { super.dispose(); } - /// 사용자가 입력한 검색어를 기반으로 게시물 새로 고침. - void refreshPostList(String targetWord) async { + /// 사용자가 입력한 검색어를 기반으로 게시물 목록 초기화 + Future _initPostList(String targetWord) async { if (targetWord == "") { if (mounted) { setState(() { @@ -101,21 +103,27 @@ class _BulletinSearchPageState extends State { return; } final UserProvider userProvider = context.read(); - final Map? myMap = await userProvider + // 타겟 단어의 1페이지 검색 결과만 불러옴. + + var response = await userProvider .getApiRes("${_apiUrl}1&main_search__contains=$targetWord"); - if (mounted && targetWord == _textEdtingController.text) { + final Map? myMap = await response?.data; + + if (mounted && myMap != null && targetWord == _textEdtingController.text) { setState(() { postPreviewList.clear(); - for (int i = 0; i < (myMap?["results"].length ?? 0); i++) { + for (int i = 0; i < (myMap["results"].length ?? 0); i++) { try { postPreviewList - .add(ArticleListActionModel.fromJson(myMap!["results"][i])); + .add(ArticleListActionModel.fromJson(myMap["results"][i])); // debugPrint("refreshPostList : postPreviewList[$i] : ${_temp[i].title}"); } catch (error) { debugPrint( "refreshPostList error at $i : $error"); // invalid json 걸러내기 } } + // 타겟 단어 1페이지 검색 결과만 불러오므로 현재 페이지를 1로 설정 + _currentPage = 1; _isLoading = false; }); } @@ -125,52 +133,64 @@ class _BulletinSearchPageState extends State { void _scrollListener() async { //스크롤 시 포커스 해제 FocusScope.of(context).unfocus(); - if (_textEdtingController.text == "") { - if (mounted) { - setState(() { - postPreviewList.clear(); - _isLoading = false; - }); - } - return; + + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent) { + _loadNextPage(_textEdtingController.text); } + } + /// 다음 페이지의 게시물을 불러옴 + /// + /// [targetWord] : api로 요청된 검색어 + /// + /// [_textEdtingController.text] : 현재 검색창에 입력된 검색어 + /// + /// 두 값을 비교해서 같을 경우에만 다음 페이지의 게시물을 빌드함. + Future _loadNextPage(String targetWord) async { + setState(() { + _isLoadingNextPage = true; + }); try { - if (mounted) { - setState(() { - _isLoadingNextPage = true; - }); - } - UserProvider userProvider = context.read(); - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent) { - _currentPage = _currentPage + 1; - //TODO: 더 이상 불러올 게시물이 없을 때의 처리 - Map? myMap = await userProvider.getApiRes( - "$_apiUrl$_currentPage&main_search__contains=${_textEdtingController.text}"); + if (_textEdtingController.text == "") { if (mounted) { setState(() { - for (int i = 0; i < (myMap!["results"].length ?? 0); i++) { - //???/ - if (myMap["results"][i]["created_by"]["profile"] != null) { - postPreviewList.add( - ArticleListActionModel.fromJson(myMap["results"][i] ?? {})); - } - } - }); - setState(() { - _isLoading = false; + postPreviewList.clear(); _isLoadingNextPage = false; }); } + return; + } + UserProvider userProvider = context.read(); + _currentPage = _currentPage + 1; + //TODO: 더 이상 불러올 게시물이 없을 때의 처리 + var response = await userProvider.getApiRes( + "$_apiUrl$_currentPage&main_search__contains=${_textEdtingController.text}"); + final Map? myMap = await response?.data; + + //비동기 함수 이후에 검색창의 검색어가 바뀌었을 경우에는 하지 않음 + if (mounted && + myMap != null && + _textEdtingController.text == targetWord) { + setState(() { + for (int i = 0; i < (myMap["results"].length ?? 0); i++) { + //???/ + if (myMap["results"][i]["created_by"]["profile"] != null) { + postPreviewList.add( + ArticleListActionModel.fromJson(myMap["results"][i] ?? {})); + } + } + // api별로 호출부터 응답 시간이 다르므로, _loadNextPage 함수 호출이 연속으로 일어나는 경우에는 게시물을 정렬해주어야함. + postPreviewList.sort((a, b) => b.created_at.compareTo(a.created_at)); + }); } } catch (error) { _currentPage = _currentPage - 1; + } + if (mounted) { setState(() { - _isLoading = false; _isLoadingNextPage = false; }); - debugPrint("scrollListener error : $error"); } } @@ -224,13 +244,19 @@ class _BulletinSearchPageState extends State { setState(() { _isLoading = true; }); - refreshPostList(text); + // 1페이지만 불러오면 한 페이지의 검색 결과의 게시물들로 태블릿의 화면을 채울 수가 없어 2페이지도 자동으로 불러오게 함 + _initPostList(text).then((value) { + _loadNextPage(text); + }); }, onChanged: (String text) { _debouncer.run(() { debugPrint( "bulletin_search_page: onChanged(${DateTime.now().toString()}) : $text"); - refreshPostList(text); + // 1페이지만 불러오면 한 페이지의 검색 결과의 게시물들로 태블릿의 화면을 채울 수가 없어 2페이지도 자동으로 불러오게 함 + _initPostList(text).then((value) { + _loadNextPage(text); + }); }); }, style: const TextStyle( @@ -313,15 +339,16 @@ class _BulletinSearchPageState extends State { // 검색어가 없을 때와 검색 결과가 없을 때의 처리 if (_textEdtingController.text == "" && postPreviewList.isEmpty) - const Expanded( + Expanded( child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - '검색어를 입력해주세요.', - style: TextStyle( + LocaleKeys.bulletinSearchPage_pleaseEnter + .tr(), + style: const TextStyle( color: Color(0xFFBBBBBB), fontSize: 16, fontWeight: FontWeight.w500, @@ -334,15 +361,15 @@ class _BulletinSearchPageState extends State { // 검색어가 있는데 검색 결과가 없을 때의 처리 if (_textEdtingController.text != "" && postPreviewList.isEmpty) - const Expanded( + Expanded( child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - '검색 결과가 없습니다.', - style: TextStyle( + LocaleKeys.bulletinSearchPage_noResults.tr(), + style: const TextStyle( color: Color(0xFFBBBBBB), fontSize: 16, fontWeight: FontWeight.w500, diff --git a/lib/pages/chat_list_page.dart b/lib/pages/chat_list_page.dart index b0b2d533..9152eefe 100644 --- a/lib/pages/chat_list_page.dart +++ b/lib/pages/chat_list_page.dart @@ -10,7 +10,7 @@ import 'package:new_ara_app/pages/chat_window_page.dart'; import 'package:new_ara_app/providers/notification_provider.dart'; class ChatListPage extends StatefulWidget { - const ChatListPage({Key? key}) : super(key: key); + const ChatListPage({super.key}); @override State createState() => _ChatListPageState(); } @@ -20,7 +20,6 @@ class _ChatListPageState extends State { // int count=0; @override void initState() { - // TODO: implement initState super.initState(); UserProvider userProvider = context.read(); context.read().checkIsNotReadExist(userProvider); @@ -68,7 +67,10 @@ class _ChatListPageState extends State { splashColor: Colors.white, icon: SvgPicture.asset( 'assets/icons/search.svg', - color: ColorsInfo.newara, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, + BlendMode.srcIn, + ), width: 45, height: 45, ), @@ -152,12 +154,12 @@ class _ChatPreviewState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 24, child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Expanded( child: Text( "카이스트 익명 밝 123 익명 123 익명 123 익명 123", diff --git a/lib/pages/chat_window_page.dart b/lib/pages/chat_window_page.dart index c02f052c..950953b3 100644 --- a/lib/pages/chat_window_page.dart +++ b/lib/pages/chat_window_page.dart @@ -3,7 +3,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:new_ara_app/constants/colors_info.dart'; class ChatWindowPage extends StatefulWidget { - const ChatWindowPage({Key? key}) : super(key: key); + const ChatWindowPage({super.key}); @override State createState() => _ChatWindowPageState(); @@ -14,7 +14,6 @@ class _ChatWindowPageState extends State { @override void initState() { - // TODO: implement initState super.initState(); for (int i = 0; i <= 3; i++) { chatBubbleList.add(const OtherChatBubble()); @@ -49,7 +48,11 @@ class _ChatWindowPageState extends State { height: 21.87, child: SvgPicture.asset( 'assets/icons/left_chevron.svg', - color: ColorsInfo.newara, + + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, + BlendMode.srcIn, + ), fit: BoxFit.fill, ), ), @@ -129,7 +132,6 @@ class DefaultInputArea extends StatefulWidget { class _DefaultInputAreaState extends State { @override void initState() { - // TODO: implement initState super.initState(); setState(() {}); } @@ -158,7 +160,10 @@ class _DefaultInputAreaState extends State { 'assets/icons/add.svg', width: 36, height: 36, - color: const Color(0xFFED3A3A), + colorFilter: const ColorFilter.mode( + Color(0xFFED3A3A), + BlendMode.srcIn, + ), ), const SizedBox( width: 7, @@ -264,7 +269,7 @@ class MyChatBubble extends StatelessWidget { } class OtherChatBubble extends StatefulWidget { - const OtherChatBubble({Key? key}) : super(key: key); + const OtherChatBubble({super.key}); @override State createState() => _OtherChatBubbleState(); @@ -346,7 +351,7 @@ class _OtherChatBubbleState extends State { } class TimeChatBubble extends StatefulWidget { - const TimeChatBubble({Key? key}) : super(key: key); + const TimeChatBubble({super.key}); @override State createState() => _TimeChatBubbleState(); @@ -366,11 +371,11 @@ class _TimeChatBubbleState extends State { borderRadius: const BorderRadius.all(Radius.circular(12.0)), ), height: 24, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 9), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 9), child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Text( "2022년 11월 24일", style: TextStyle( diff --git a/lib/pages/error_page.dart b/lib/pages/error_page.dart index bb8b929b..cb721bcd 100644 --- a/lib/pages/error_page.dart +++ b/lib/pages/error_page.dart @@ -12,9 +12,9 @@ class _ErrorPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Error Page'), + title: const Text('Error Page'), ), - body: Center( + body: const Center( child: Text('Error Page'), ), ); diff --git a/lib/pages/inquiry_page.dart b/lib/pages/inquiry_page.dart index be382466..ec2ecfba 100644 --- a/lib/pages/inquiry_page.dart +++ b/lib/pages/inquiry_page.dart @@ -1,10 +1,10 @@ -import 'dart:io'; - +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:new_ara_app/constants/colors_info.dart'; import 'package:new_ara_app/providers/user_provider.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -13,14 +13,14 @@ import 'package:url_launcher/url_launcher.dart'; ///TODO: class 명과 파일명은 변경 필요 /// ///TODO: 디자인 변경 필요 -class InQuiryPage extends StatefulWidget { - const InQuiryPage({super.key}); +class InquiryPage extends StatefulWidget { + const InquiryPage({super.key}); @override - State createState() => _InQuiryPageState(); + State createState() => _InquiryPageState(); } -class _InQuiryPageState extends State { +class _InquiryPageState extends State { @override Widget build(BuildContext context) { UserProvider userProvider = context.watch(); @@ -28,10 +28,10 @@ class _InQuiryPageState extends State { backgroundColor: Colors.white, appBar: AppBar( centerTitle: true, - title: const SizedBox( + title: SizedBox( child: Text( - "문의 페이지", - style: TextStyle( + LocaleKeys.inquiryPage_title.tr(), + style: const TextStyle( color: ColorsInfo.newara, fontSize: 18, fontWeight: FontWeight.w700, @@ -62,25 +62,51 @@ class _InQuiryPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text.rich( + Container( + width: MediaQuery.of(context).size.width, + margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: Colors.red, + width: 3.5, // Border width + ), + ), + + child : Text.rich( TextSpan( - text: '이미 탈퇴 했던 계정입니다.\n재가입을 하고 싶다면 이메일로 문의하세요.', + text: LocaleKeys.inquiryPage_reLoginErrorWithWithdrawalGuide + .tr(), style: const TextStyle( - decoration: TextDecoration.underline, - color: Colors.blue, fontSize: 20), + color: Colors.red, + fontSize: 23.0, + fontWeight: FontWeight.bold, + //decoration : TextDecoration.underline, + ), recognizer: TapGestureRecognizer() ..onTap = () async { int? userID = userProvider.naUser!.user; String? email = userProvider.naUser?.email; String? nickname = userProvider.naUser?.nickname; - final String body = - """여기 아래에 문의 사항을 적어주세요.\n\n※ Ara 관리자가 48시간 이내로 답변드립니다.※\n\n유저 번호: $userID\n닉네임: $nickname\n이메일: $email\n플랫폼: App\n"""; + final String body = LocaleKeys + .inquiryPage_reLoginErrorWithWithdrawalEmailContents + .tr( + namedArgs: { + 'userID': userID.toString(), + 'email': email.toString(), + 'nickname': nickname.toString(), + }, + ); final emailLaunchUri = Uri( scheme: 'mailto', path: 'ara@sparcs.org', query: encodeQueryParameters({ - 'subject': 'Ara에 문의합니다', + 'subject': LocaleKeys + .inquiryPage_reLoginErrorWithWithdrawalEmailTitle + .tr(), 'body': body, }), ); @@ -94,6 +120,7 @@ class _InQuiryPageState extends State { }, ), ), + ), ], ), ), diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 45aaeca9..ad84e09f 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -5,13 +5,13 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:new_ara_app/constants/colors_info.dart'; import 'package:new_ara_app/pages/sparcs_sso_page.dart'; -import 'package:new_ara_app/pages/terms_and_conditions_page.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/utils/slide_routing.dart'; /// `LoginPage` 위젯은 사용자에게 로그인 페이지를 표시. class LoginPage extends StatefulWidget { /// 기본 생성자입니다. - const LoginPage({Key? key}) : super(key: key); + const LoginPage({super.key}); @override State createState() => _LoginPageState(); @@ -54,7 +54,7 @@ class _LoginPageState extends State { ), child: TextButton( child: Text( - 'login_page.login'.tr(), + LocaleKeys.loginPage_login.tr(), style: const TextStyle( color: Colors.white, fontSize: 18, diff --git a/lib/pages/main_navigation_tab_page.dart b/lib/pages/main_navigation_tab_page.dart index fc07a0fd..58919aea 100644 --- a/lib/pages/main_navigation_tab_page.dart +++ b/lib/pages/main_navigation_tab_page.dart @@ -4,8 +4,7 @@ import 'package:provider/provider.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:new_ara_app/pages/main_page.dart'; -import 'package:new_ara_app/pages/bulletin_list_page.dart'; -import 'package:new_ara_app/pages/chat_list_page.dart'; +import 'package:new_ara_app/pages/board_list_page.dart'; import 'package:new_ara_app/pages/notification_page.dart'; import 'package:new_ara_app/pages/user_page.dart'; import 'package:new_ara_app/providers/notification_provider.dart'; @@ -14,7 +13,7 @@ import 'package:new_ara_app/constants/colors_info.dart'; /// MainNavigationTabPage /// 메인 화면 하단에 위치하는 탭바를 포함한 메인 페이지. class MainNavigationTabPage extends StatefulWidget { - const MainNavigationTabPage({Key? key}) : super(key: key); + const MainNavigationTabPage({super.key}); @override State createState() => _MainNavigationTabPageState(); } @@ -25,7 +24,7 @@ class _MainNavigationTabPageState extends State { // 탭별로 연결될 페이지 목록 final List _widgetOptions = [ const MainPage(), - const BulletinListPage(), + const BoardListPage(), //const ChatListPage(), const NotificationPage(), const UserPage(), diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 56e47291..7440ff3e 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:new_ara_app/pages/bulletin_search_page.dart'; -import 'package:new_ara_app/pages/inquiry_page.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:provider/provider.dart'; import 'package:new_ara_app/constants/board_type.dart'; @@ -17,11 +17,10 @@ import 'package:new_ara_app/models/article_list_action_model.dart'; import 'package:new_ara_app/pages/post_view_page.dart'; import 'package:new_ara_app/utils/slide_routing.dart'; import 'package:new_ara_app/providers/notification_provider.dart'; -import 'package:new_ara_app/utils/handle_hidden.dart'; import 'package:new_ara_app/utils/cache_function.dart'; class MainPage extends StatefulWidget { - const MainPage({Key? key}) : super(key: key); + const MainPage({super.key}); @override State createState() => _MainPageState(); } @@ -96,7 +95,6 @@ class _MainPageState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) async { - UserProvider userProvider = context.read(); // 앱이 포그라운드로 전환될 때 실행할 함수 if (state == AppLifecycleState.resumed) { //api를 호출 후 최신 데이터로 갱신 @@ -288,7 +286,8 @@ class _MainPageState extends State with WidgetsBindingObserver { Future _refreshTopContents(UserProvider userProvider, List contentList) async { String apiUrl = 'articles/top/'; - final dynamic response = await userProvider.getApiRes(apiUrl); + var getResponse = await userProvider.getApiRes(apiUrl); + final Map? response = await getResponse?.data; if (mounted && response != null) { setState(() { contentList.clear(); @@ -317,7 +316,8 @@ class _MainPageState extends State with WidgetsBindingObserver { String apiUrl = topicID == -1 ? "articles/?parent_board=$boardID" : "articles/?parent_board=$boardID&parent_topic=$topicID"; - final dynamic response = await userProvider.getApiRes(apiUrl); + var getResponse = await userProvider.getApiRes(apiUrl); + final dynamic response = await getResponse?.data; if (mounted && response != null) { setState(() { contentList.clear(); @@ -363,7 +363,7 @@ class _MainPageState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { - UserProvider userProvider = context.watch(); + debugPrint("build invoked!!"); return Scaffold( appBar: AppBar( elevation: 0, @@ -372,6 +372,18 @@ class _MainPageState extends State with WidgetsBindingObserver { fit: BoxFit.cover, ), actions: [ + IconButton( + onPressed: () async { + if (context.locale == const Locale('ko')) { + await context.setLocale(const Locale('en')); + } else { + await context.setLocale(const Locale('ko')); + } + }, + icon: const Icon( + Icons.language, + color: ColorsInfo.newara, + )), IconButton( icon: SvgPicture.asset( 'assets/icons/post.svg', @@ -418,6 +430,7 @@ class _MainPageState extends State with WidgetsBindingObserver { _isLoading[11] ? const LoadingIndicator() : RefreshIndicator.adaptive( + displacement: 0.0, color: ColorsInfo.newara, onRefresh: () async { //api를 호출 후 최신 데이터로 갱신 @@ -452,7 +465,7 @@ class _MainPageState extends State with WidgetsBindingObserver { return Column( children: [ MainPageTextButton( - 'main_page.realtime', + LocaleKeys.mainPage_topPost.tr(), () async { await Navigator.of(context).push(slideRoute(const PostListShowPage( boardType: BoardType.top, @@ -518,7 +531,7 @@ class _MainPageState extends State with WidgetsBindingObserver { return Column( children: [ MainPageTextButton( - '자유게시판', + LocaleKeys.mainPage_talk.tr(), () async { await Navigator.of(context).push(slideRoute(PostListShowPage( boardType: BoardType.free, @@ -578,9 +591,9 @@ class _MainPageState extends State with WidgetsBindingObserver { children: [ SizedBox( width: MediaQuery.of(context).size.width - 40, - child: const Text( - '공지', - style: TextStyle( + child: Text( + LocaleKeys.mainPage_notice.tr(), + style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 20, color: Colors.black, @@ -623,9 +636,9 @@ class _MainPageState extends State with WidgetsBindingObserver { const SizedBox( width: 5, ), - const Text( - "포탈 공지", - style: TextStyle( + Text( + LocaleKeys.mainPage_portalNotice.tr(), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: Color(0xFF1F4899), @@ -709,9 +722,9 @@ class _MainPageState extends State with WidgetsBindingObserver { }, child: Row( children: [ - const Text( - "입주 업체", - style: TextStyle( + Text( + LocaleKeys.mainPage_facility.tr(), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: Color(0xFF646464), @@ -761,9 +774,9 @@ class _MainPageState extends State with WidgetsBindingObserver { }, child: Row( children: [ - const Text( - "Ara 운영진", - style: TextStyle( + Text( + LocaleKeys.mainPage_araAdmins.tr(), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: Color(0xFFED3A3A), @@ -812,9 +825,9 @@ class _MainPageState extends State with WidgetsBindingObserver { children: [ SizedBox( width: MediaQuery.of(context).size.width - 40, - child: const Text( - '거래', - style: TextStyle( + child: Text( + LocaleKeys.mainPage_trades.tr(), + style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 20, color: Colors.black, @@ -846,9 +859,9 @@ class _MainPageState extends State with WidgetsBindingObserver { }, child: Row( children: [ - const Text( - "부동산", - style: TextStyle( + Text( + LocaleKeys.mainPage_realEstate.tr(), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: Color(0xFF4A90E2), @@ -901,9 +914,9 @@ class _MainPageState extends State with WidgetsBindingObserver { }, child: Row( children: [ - const Text( - "중고거래", - style: TextStyle( + Text( + LocaleKeys.mainPage_market.tr(), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: Color(0xFF646464), @@ -954,9 +967,9 @@ class _MainPageState extends State with WidgetsBindingObserver { }, child: Row( children: [ - const Text( - "구인구직", - style: TextStyle( + Text( + LocaleKeys.mainPage_jobsWanted.tr(), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: Color(0xFFED3A3A), @@ -1004,7 +1017,8 @@ class _MainPageState extends State with WidgetsBindingObserver { Widget _buildStuCommunityContents() { return Column( children: [ - MainPageTextButton('main_page.stu_community', () async { + MainPageTextButton(LocaleKeys.mainPage_organizationsAndClubs.tr(), + () async { await Navigator.of(context).push(slideRoute(PostListShowPage( boardType: BoardType.free, boardInfo: _searchBoard("students-group")))); @@ -1028,9 +1042,9 @@ class _MainPageState extends State with WidgetsBindingObserver { children: [ Row( children: [ - const Text( - '원총', - style: TextStyle( + Text( + LocaleKeys.mainPage_gradAssoc.tr(), + style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 14, color: Color.fromRGBO(177, 177, 177, 1), @@ -1060,9 +1074,9 @@ class _MainPageState extends State with WidgetsBindingObserver { ), Row( children: [ - const Text( - '총학', - style: TextStyle( + Text( + LocaleKeys.mainPage_undergradAssoc.tr(), + style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 14, color: Color.fromRGBO(177, 177, 177, 1), @@ -1092,9 +1106,9 @@ class _MainPageState extends State with WidgetsBindingObserver { ), Row( children: [ - const Text( - '새학', - style: TextStyle( + Text( + LocaleKeys.mainPage_freshmanCouncil.tr(), + style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 14, color: Color.fromRGBO(177, 177, 177, 1), @@ -1206,7 +1220,7 @@ class MainPageTextButton extends StatelessWidget { child: Row( children: [ Text( - buttonTitle.tr(), + buttonTitle, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 20, @@ -1249,6 +1263,47 @@ class LittleText extends StatelessWidget { this.showTopic = false, }); + /// 게시글 정보를 입력받고 그에 상태에 따라 적절한 제목을 리턴하는 함수. + /// UserViewPage, UserPage, PostListShowPage, PostViewPage에서 사용함. + String getTitle( + String? orignialTitle, bool isHidden, List whyHidden) { + // 숨겨진 글이 아닌 경우 + if (isHidden == false) { + return orignialTitle.toString(); + } + // 숨겨졌으나 why_hidden이 지정되지 않은 경우. 혹시 모를 에러 방지를 위해 추가함. + else if (whyHidden.isEmpty) { + return LocaleKeys.postPreview_hiddenPost.tr(); + } + + // TODO: 새로운 사유가 있을 경우 코드에 반영하기. + late String title; + switch (whyHidden[0]) { + case "REPORTED_CONTENT": + title = LocaleKeys.postPreview_reportedPost.tr(); + break; + case "BLOCKED_USER_CONTENT": + title = LocaleKeys.postPreview_blockedUsersPost.tr(); + break; + case "ADULT_CONTENT": + title = LocaleKeys.postPreview_adultPost.tr(); + break; + case "SOCIAL_CONTENT": + title = LocaleKeys.postPreview_socialPost.tr(); + break; + case "ACCESS_DENIED_CONTENT": + title = LocaleKeys.postPreview_accessDeniedPost.tr(); + break; + // 새로운 whyHidden에 대해서는 숨겨진 게시글로 표기. 이후 앱에서 반영해줘야 함. + default: + debugPrint( + "\n***********************\nANOTHER HIDDEN REASON FOUND: ${whyHidden[0]}\n***********************\n"); + title = LocaleKeys.postPreview_hiddenPost.tr(); + } + + return title; + } + @override Widget build(BuildContext context) { return Text.rich( @@ -1258,7 +1313,9 @@ class LittleText extends StatelessWidget { children: [ if (content.parent_topic != null && showTopic) TextSpan( - text: "[${content.parent_topic!.ko_name}] ", + text: context.locale == const Locale('ko') + ? "[${content.parent_topic!.ko_name}] " + : "[${content.parent_topic!.en_name}] ", style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w400, diff --git a/lib/pages/notification_page.dart b/lib/pages/notification_page.dart index bcc05496..fc42e53c 100644 --- a/lib/pages/notification_page.dart +++ b/lib/pages/notification_page.dart @@ -1,13 +1,13 @@ -/// 사용자에 대한 이때까지의 알림을 보여주는 페이지 관리 파일 +// 사용자에 대한 이때까지의 알림을 보여주는 페이지 관리 파일 import 'dart:async'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:new_ara_app/utils/create_dio_with_config.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:provider/provider.dart'; -import 'package:dio/dio.dart'; import 'package:new_ara_app/constants/url_info.dart'; +import 'package:dio/dio.dart'; import 'package:new_ara_app/widgets/loading_indicator.dart'; import 'package:new_ara_app/constants/colors_info.dart'; import 'package:new_ara_app/providers/user_provider.dart'; @@ -19,7 +19,7 @@ import 'package:new_ara_app/widgets/snackbar_noti.dart'; /// 알림페이지의 빌드 및 이벤트 처리를 담당하는 위젯. class NotificationPage extends StatefulWidget { - const NotificationPage({Key? key}) : super(key: key); + const NotificationPage({super.key}); @override State createState() => _NotificationPageState(); } @@ -66,12 +66,13 @@ class _NotificationPageState extends State { /// 결과 리스트를 반환함. Future> _fetchEachPage( UserProvider userProvider, int targetPage) async { - Dio dio = userProvider.createDioWithHeadersForGet(); List resList = []; try { - var response = await dio - .get("$newAraDefaultUrl/api/notifications/?page=$targetPage"); - List resultsList = response.data["results"]; + var response = + await userProvider.getApiRes("notifications/?page=$targetPage"); + final Map? jsonList = await response?.data; + + List resultsList = jsonList?["results"]; for (var json in resultsList) { try { resList.add(NotificationModel.fromJson(json)); @@ -99,15 +100,15 @@ class _NotificationPageState extends State { bool hasNext = true; UserProvider userProvider = context.read(); List resList = []; - Dio dio = userProvider.createDioWithHeadersForGet(); int page = 1; for (page = 1; hasNext; page++) { if (page > _curPage + 1) break; try { var response = - await dio.get("$newAraDefaultUrl/api/notifications/?page=$page"); - hasNext = response.data["next"] == null ? false : true; - List resultsList = response.data["results"]; + await userProvider.getApiRes("notifications/?page=$page"); + final Map? jsonList = await response?.data; + hasNext = jsonList?["next"] == null ? false : true; + List resultsList = jsonList?["results"]; for (var json in resultsList) { try { resList.add(NotificationModel.fromJson(json)); @@ -144,12 +145,10 @@ class _NotificationPageState extends State { /// API 통신에 필요한 [userProvider], 알림 식별에 필요한 [id]를 전달받음. /// 읽음 처리가 성공하면 true, 그렇지 않으면 false 리턴. Future _readNotification(UserProvider userProvider, int id) async { - try { - await userProvider - .createDioWithHeadersForNonget() - .post("$newAraDefaultUrl/api/notifications/$id/read/"); - } catch (error) { - debugPrint("POST /api/notifications/$id/read/ failed: $error"); + Response? postRes = + await userProvider.postApiRes("notifications/$id/read/"); + if (postRes == null) { + debugPrint("POST /api/notifications/$id/read/ failed"); return false; } return true; @@ -159,12 +158,10 @@ class _NotificationPageState extends State { /// API 통신을 위해 [userProvider]를 이용함. /// 모두 읽음 처리 성공 시에 true, 아니면 false를 반환함. Future _readAllNotification(UserProvider userProvider) async { - try { - await userProvider - .createDioWithHeadersForNonget() - .post("$newAraDefaultUrl/api/notifications/read_all/"); - } catch (error) { - debugPrint("POST /api/notifications/read_all failed: $error"); + Response? postRes = + await userProvider.postApiRes("notifications/read_all/"); + if (postRes == null) { + debugPrint("POST /api/notifications/read_all failed"); return false; } return true; @@ -179,7 +176,7 @@ class _NotificationPageState extends State { appBar: AppBar( centerTitle: false, title: Text( - 'appBar.notification'.tr(), + LocaleKeys.notificationPage_notifications.tr(), style: const TextStyle( fontSize: 28, fontWeight: FontWeight.w700, @@ -225,9 +222,11 @@ class _NotificationPageState extends State { BlendMode.srcIn, ), ), - const Text( - '알림이 없습니다.', - style: TextStyle( + Text( + LocaleKeys + .notificationPage_noNotifications + .tr(), + style: const TextStyle( color: Color(0xFFBBBBBB), fontSize: 15, ), @@ -257,11 +256,13 @@ class _NotificationPageState extends State { ? _buildDateInfo( _modelList[idx - 1].created_at, _modelList[idx].created_at) - : const SizedBox( + : SizedBox( height: 35, child: Text( - '오늘', - style: TextStyle( + getFormattedDate( + _modelList[0].created_at, + context.locale), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Color.fromRGBO( @@ -313,7 +314,7 @@ class _NotificationPageState extends State { decoration: BoxDecoration( border: Border.all( width: 1, - color: Color(0xfff0f0f0), + color: const Color(0xfff0f0f0), ), borderRadius: const BorderRadius.all( @@ -343,7 +344,8 @@ class _NotificationPageState extends State { color: (targetNoti .is_read ?? false) - ? Color(0xffbbbbbb) + ? const Color( + 0xffbbbbbb) : ColorsInfo.newara, ), child: Center( @@ -372,8 +374,11 @@ class _NotificationPageState extends State { CrossAxisAlignment .start, children: [ + //TODO: 나중에 백엔드에서 보내주는 내용으로 보여줘야함. Text( - targetNoti.title, + LocaleKeys + .notificationPage_newComment + .tr(), maxLines: 1, overflow: TextOverflow .ellipsis, @@ -400,7 +405,7 @@ class _NotificationPageState extends State { ), ), Text( - "| 게시글: ${targetNoti.related_article.title}", + "| ${LocaleKeys.notificationPage_post.tr()}: ${targetNoti.related_article.title}", maxLines: 1, overflow: TextOverflow .ellipsis, @@ -452,7 +457,8 @@ class _NotificationPageState extends State { _setIsLoadingTotal(false); } } else { - requestInfoSnackBar("이미 알림을 모두 읽으셨습니다."); + requestInfoSnackBar( + LocaleKeys.notificationPage_allNotificationsChecked.tr()); } }, backgroundColor: Colors.white, @@ -473,6 +479,29 @@ class _NotificationPageState extends State { ); } + /// 가장 상단 알림의 날짜 출력을 위해 사용됨. + String getFormattedDate(String dateTimeStr, Locale locale) { + DateTime targetDateTime = DateTime.parse(dateTimeStr).toLocal(); + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + if (targetDateTime.year == today.year && + targetDateTime.month == today.month && + targetDateTime.day == today.day) { + return LocaleKeys.notificationPage_today.tr(); + } else { + String dateTextInKorean = + "${(targetDateTime.year != now.year ? "${targetDateTime.year}년 " : "")}${targetDateTime.month}월 ${targetDateTime.day}일"; + + String dateTextInEnglish = DateFormat( + "MMMM d${targetDateTime.year != now.year ? ", yyyy" : ""}", + "en_US") + .format(targetDateTime); + return (locale == const Locale('ko') + ? dateTextInKorean + : dateTextInEnglish); + } + } + /// 현재 date와 알림 생성 date의 차이를 계산하여 문자열로 변경해줌. Widget _buildDateInfo(String strDate1, String strDate2) { DateTime now = DateTime.now(); @@ -481,13 +510,21 @@ class _NotificationPageState extends State { if (prevDate.year == curDate.year && prevDate.month == curDate.month && prevDate.day == curDate.day) return Container(); - String dateText = + + String dateTextInKorean = "${(curDate.year != now.year ? "${curDate.year}년 " : "")}${curDate.month}월 ${curDate.day}일"; + + String dateTextInEnglish = + DateFormat("MMMM d${curDate.year != now.year ? ", yyyy" : ""}", "en_US") + .format(curDate); + return SizedBox( height: 60, child: Center( child: Text( - dateText, + context.locale.languageCode == "ko" + ? dateTextInKorean + : dateTextInEnglish, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, diff --git a/lib/pages/post_list_show_page.dart b/lib/pages/post_list_show_page.dart index d76a32f2..00b2f092 100644 --- a/lib/pages/post_list_show_page.dart +++ b/lib/pages/post_list_show_page.dart @@ -1,9 +1,9 @@ -import 'dart:math'; - +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:new_ara_app/pages/bulletin_search_page.dart'; import 'package:new_ara_app/pages/post_write_page.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:provider/provider.dart'; import 'package:new_ara_app/constants/board_type.dart'; @@ -16,6 +16,7 @@ import 'package:new_ara_app/models/article_list_action_model.dart'; import 'package:new_ara_app/pages/post_view_page.dart'; import 'package:new_ara_app/utils/slide_routing.dart'; import 'package:new_ara_app/providers/notification_provider.dart'; +import 'package:new_ara_app/widgets/pop_up_menu_buttons.dart'; /// PostListShowPage는 게시물 목록를 나타내는 위젯. /// boardType에 따라 게시판의 종류를 판별하고, 특성화 된 위젯들을 활성화 비활성화 되도록 설계. @@ -47,43 +48,63 @@ class _PostListShowPageState extends State super.initState(); var userProvider = context.read(); + _scrollController.addListener(_scrollListener); + WidgetsBinding.instance.addObserver(this); + // updateAllBulletinList().then( + // (value) { + // _loadNextPage(); + // }, + // ); + context.read().checkIsNotReadExist(userProvider); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + /// initState 직후에는 updateAllBulletinList가 호출되어야 함. + /// initState 직후에는 apiUrl == ""이므로 이를 통해 + /// didChangeDependencies에서 initState 직후 임을 파악하고 updateAllBulletinList 함수를 호출함. + String prevApiUrl = apiUrl; // 게시판 타입에 따라 API URL과 게시판 이름을 설정 + debugPrint("Current Locale: ${context.locale}"); switch (widget.boardType) { case BoardType.free: apiUrl = "articles/?parent_board=${widget.boardInfo!.id.toInt()}&page="; - _boardName = widget.boardInfo!.ko_name; + _boardName = context.locale == const Locale("ko") + ? widget.boardInfo!.ko_name + : widget.boardInfo!.en_name; break; case BoardType.recent: apiUrl = "articles/recent/?page="; - _boardName = "최근 본 글"; + _boardName = LocaleKeys.postListShowPage_history.tr(); break; case BoardType.top: apiUrl = "articles/top/?page="; - _boardName = "실시간 인기글"; + _boardName = LocaleKeys.postListShowPage_topPosts.tr(); break; case BoardType.all: apiUrl = "articles/?page="; - _boardName = "전체보기"; + _boardName = LocaleKeys.postListShowPage_allPosts.tr(); break; case BoardType.scraps: apiUrl = "scraps/?page="; - _boardName = "담아둔 글"; + _boardName = LocaleKeys.postListShowPage_bookmarks.tr(); break; default: apiUrl = "articles/?page="; - _boardName = "테스트 게시판"; + _boardName = LocaleKeys.postListShowPage_testBoard.tr(); break; } - - _scrollController.addListener(_scrollListener); - WidgetsBinding.instance.addObserver(this); - updateAllBulletinList().then( - (value) { - _loadNextPage(); - }, - ); - context.read().checkIsNotReadExist(userProvider); + // initState 직후에는 updateAllBulletinList 함수를 호출함. + if (prevApiUrl == "") { + updateAllBulletinList().then( + (_) { + _loadNextPage(); + }, + ); + } } @override @@ -107,6 +128,7 @@ class _PostListShowPageState extends State /// [pageLimitToReload] : 새로 로딩할 최대 페이지 수. /// [currentPage] 값보다 [pageLimitToReload] 값이 큰 지 코딩할 때 주의 바랍니다. Future updateAllBulletinList({int? pageLimitToReload}) async { + debugPrint("updateAllBulletinList called!!!!!!"); List newList = []; UserProvider userProvider = context.read(); @@ -116,7 +138,8 @@ class _PostListShowPageState extends State // pageLimitToReload가 null이 아니면(파라미터가 존재하면) currentPage보다 우선시 합니다. (pageLimitToReload ?? currentPage); page++) { - Map? json = await userProvider.getApiRes("$apiUrl$page"); + var response = await userProvider.getApiRes("$apiUrl$page"); + final Map? json = await response?.data; if (json != null && json.containsKey("results")) { for (var result in json["results"]) { @@ -133,6 +156,7 @@ class _PostListShowPageState extends State } } + debugPrint("updateAllBulleinList mounted: $mounted"); // 위젯이 마운트 상태인 경우 상태를 업데이트합니다. if (mounted) { setState(() { @@ -148,6 +172,7 @@ class _PostListShowPageState extends State /// 다음 페이지를 로드하는 함수 Future _loadNextPage() async { var userProvider = context.read(); + setState(() { _isLoadingNextPage = true; }); @@ -156,8 +181,9 @@ class _PostListShowPageState extends State // await Future.delayed(Duration(seconds: 1)); try { currentPage = currentPage + 1; - Map? myMap = - await userProvider.getApiRes("$apiUrl$currentPage"); + var response = await userProvider.getApiRes("$apiUrl$currentPage"); + final Map? myMap = await response?.data; + if (mounted) { setState(() { for (int i = 0; i < (myMap!["results"].length ?? 0); i++) { @@ -170,6 +196,7 @@ class _PostListShowPageState extends State myMap["results"][i]["parent_article"] ?? {})); } } + postPreviewList.sort((a, b) => b.created_at.compareTo(a.created_at)); }); } } catch (error) { @@ -214,11 +241,11 @@ class _PostListShowPageState extends State width: 35, height: 35, ), - const Padding( - padding: EdgeInsets.only(left: 29), + Padding( + padding: const EdgeInsets.only(left: 29), child: Text( - "게시판", - style: TextStyle( + LocaleKeys.postListShowPage_boards.tr(), + style: const TextStyle( color: Color(0xFFED3A3A), fontSize: 17, fontWeight: FontWeight.w500, @@ -239,6 +266,8 @@ class _PostListShowPageState extends State ), ), actions: [ + if (widget.boardInfo?.slug == 'with-school') + const WithSchoolPopupMenuButton(), IconButton( icon: SvgPicture.asset( 'assets/icons/search.svg', @@ -272,7 +301,7 @@ class _PostListShowPageState extends State updateAllBulletinList(); debugPrint('FloatingActionButton pressed'); }, - backgroundColor: Colors.white, + backgroundColor: ColorsInfo.newara, child: SizedBox( width: 42, height: 42, @@ -280,7 +309,7 @@ class _PostListShowPageState extends State 'assets/icons/modify.svg', fit: BoxFit.fill, colorFilter: const ColorFilter.mode( - ColorsInfo.newara, BlendMode.srcIn), // 글쓰기 아이콘 색상 변경 + Colors.white, BlendMode.srcIn), // 글쓰기 아이콘 색상 변경 ), ), ), @@ -290,17 +319,28 @@ class _PostListShowPageState extends State ], ), ), - body: isLoading + body: isLoading // 페이지를 처음 로드할 때만 LoadingIndicator()를 부름. ? const LoadingIndicator() : SafeArea( child: Center( child: SizedBox( width: MediaQuery.of(context).size.width - 18, child: RefreshIndicator.adaptive( + displacement: 0.0, color: ColorsInfo.newara, onRefresh: () async { - setState((() => isLoading = true)); - await updateAllBulletinList(pageLimitToReload: 1); + // refresh 중에는 LoadingIndicator를 사용하지 않으므로 setState()는 제거함. + // 로직상 isLoading 변수의 값은 상황에 맞게 변경되도록 함. + isLoading = true; + // 리프레쉬시 게시물 목록을 업데이트합니다. + // 1페이지만 로드하도록 설정하여 최신 게시물을 불러옵니다. + // 1페이지만 로드하면 태블릿에서 게시물로 화면을 꽉채우지 못하므로 다음 페이지도 로드합니다. + await updateAllBulletinList(pageLimitToReload: 1).then( + (value) { + _loadNextPage(); + }, + ); + isLoading = false; }, child: ListView.separated( physics: const AlwaysScrollableScrollPhysics(), @@ -311,6 +351,7 @@ class _PostListShowPageState extends State // 각 아이템을 위한 위젯 생성 if (_isLoadingNextPage && index == postPreviewList.length) { + debugPrint('Next Page Load Request'); return const SizedBox( height: 50, child: Center( diff --git a/lib/pages/post_view_page.dart b/lib/pages/post_view_page.dart index b997a673..f9fa1af8 100644 --- a/lib/pages/post_view_page.dart +++ b/lib/pages/post_view_page.dart @@ -1,13 +1,14 @@ -/// post의 내용을 보여주는 페이지 전체를 관리하는 파일. -/// 뷰, 이벤트 처리 모두를 관리하고 있음. +// post의 내용을 보여주는 페이지 전체를 관리하는 파일. +// 뷰, 이벤트 처리 모두를 관리하고 있음. import 'dart:core'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:new_ara_app/constants/url_info.dart'; import 'package:provider/provider.dart'; import 'package:dio/dio.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/constants/colors_info.dart'; import 'package:new_ara_app/models/article_model.dart'; @@ -26,9 +27,11 @@ import 'package:new_ara_app/widgets/in_article_web_view.dart'; import 'package:new_ara_app/providers/notification_provider.dart'; import 'package:new_ara_app/widgets/pop_up_menu_buttons.dart'; import 'package:new_ara_app/utils/profile_image.dart'; -import 'package:new_ara_app/utils/handle_hidden.dart'; import 'package:new_ara_app/widgets/snackbar_noti.dart'; import 'package:new_ara_app/providers/blocked_provider.dart'; +import 'package:new_ara_app/utils/handle_hidden.dart'; +import 'package:new_ara_app/utils/handle_name.dart'; +import 'package:new_ara_app/utils/with_school.dart'; // TODO: Dio 사용방식 createDioWithHeaders~ 로 변경하기 @@ -128,16 +131,15 @@ class _PostViewPageState extends State { /// 기존에 차단된 글에서는 title, content 등이 null이지만 override_hidden이 true이면 원래 내용이 로드됨. /// _article, _commentList, _commentKeys의 값이 모두 설정되면 true, 아닌 경우 false 반환. Future _fetchArticle(UserProvider userProvider, - {override_hidden = false}) async { - dynamic articleJson; - String apiUrl = "$newAraDefaultUrl/api/articles/${widget.articleID}"; + {overrideHidden = false}) async { + Map? articleJson; + String apiUrl = "articles/${widget.articleID}"; // 차단된 유저의 글에 대한 내용을 로드하는 경우 주소를 수정함. - if (override_hidden) apiUrl += "/?override_hidden=true"; + if (overrideHidden) apiUrl += "/?override_hidden=true"; try { - var response = - await userProvider.createDioWithHeadersForGet().get(apiUrl); - articleJson = response.data; + var response = await userProvider.getApiRes(apiUrl); + articleJson = await response?.data; } on DioException catch (e) { debugPrint("DioException occurred"); if (e.response != null) { @@ -236,7 +238,11 @@ class _PostViewPageState extends State { Padding( padding: const EdgeInsets.only(left: 29), child: Text( - _isPageLoaded ? _article.parent_board.ko_name : "", + _isPageLoaded + ? (context.locale == const Locale('ko') + ? _article.parent_board.ko_name + : _article.parent_board.en_name) + : "", style: const TextStyle( color: Color(0xFFED3A3A), fontSize: 17, @@ -294,8 +300,11 @@ class _PostViewPageState extends State { _article.url.toString()) .then((launchRes) { if (launchRes == false) { - showInfoBySnackBar(context, - '브라우저로 URL을 열 수 없습니다.'); + showInfoBySnackBar( + context, + LocaleKeys + .postViewPage_launchInBrowserNotAvailable + .tr()); } }); }, @@ -311,7 +320,8 @@ class _PostViewPageState extends State { const BorderRadius.all( Radius.circular(10)), border: Border.all( - color: Color(0xFFDBDBDB), + color: + const Color(0xFFDBDBDB), width: 1), boxShadow: const [ BoxShadow( @@ -436,10 +446,11 @@ class _PostViewPageState extends State { .contains(_article .created_by.id .toString())) - ? "차단한 사용자의 게시물입니다.\n" + ? "${LocaleKeys.postViewPage_blockedUsersPost.tr()}\n" : getAllHiddenReasons( _article - .why_hidden) + .why_hidden, + context.locale) .join('\n'), textAlign: TextAlign.center, style: const TextStyle( @@ -472,7 +483,11 @@ class _PostViewPageState extends State { .can_override_hidden == true, child: Container( - width: 104, + width: context.locale == + const Locale( + 'ko') + ? 104 + : 150, height: 36, decoration: BoxDecoration( @@ -491,14 +506,17 @@ class _PostViewPageState extends State { onTap: () async { await _fetchArticle( userProvider, - override_hidden: + overrideHidden: true); _updateState(); }, - child: const Center( + child: Center( child: Text( - '숨긴내용 보기', - style: TextStyle( + LocaleKeys + .postViewPage_showHiddenPosts + .tr(), + style: + const TextStyle( color: Color( 0xff4a4a4a), fontWeight: @@ -543,7 +561,7 @@ class _PostViewPageState extends State { MediaQuery.of(context).size.width - 40, child: Text( - '${_article.comment_count}개의 댓글', + '${_article.comment_count}${LocaleKeys.postViewPage_displayCommentCount.tr()}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, @@ -583,7 +601,8 @@ class _PostViewPageState extends State { children: [ if (_article.parent_topic != null) TextSpan( - text: "[${_article.parent_topic!.ko_name}] ", + text: + "[${context.locale == const Locale('ko') ? _article.parent_topic!.ko_name : _article.parent_topic!.en_name}] ", style: const TextStyle( color: Color(0xFFED3A3A), fontWeight: FontWeight.w700, @@ -595,7 +614,7 @@ class _PostViewPageState extends State { text: (_isAnonymousIOS(_article) && blockedProvider.blockedAnonymousPostIDs .contains(_article.created_by.id.toString())) - ? "차단한 사용자의 게시물입니다." + ? LocaleKeys.postViewPage_blockedUsersPost.tr() : getTitle(_article.title, _article.is_hidden, _article.why_hidden), style: const TextStyle( @@ -616,7 +635,7 @@ class _PostViewPageState extends State { Row( children: [ Text( - specificTime(_article.created_at), + specificTime(_article.created_at, context.locale), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, @@ -625,13 +644,16 @@ class _PostViewPageState extends State { ), const SizedBox(width: 10), Text( - '조회 ${_article.hit_count}', + '${LocaleKeys.postViewPage_hit.tr()} ${_article.hit_count}', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFFBBBBBB), ), ), + if (_article.parent_board.slug == 'with-school') + buildWithSchoolStatusBox( + _article.communication_article_status), ], ), // 좋아요, 싫어요, 댓글 갯수 표시 Row @@ -730,7 +752,10 @@ class _PostViewPageState extends State { constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width - 150), child: Text( - _article.created_by.profile.nickname.toString(), + getName( + _article.name_type, + _article.created_by.profile.nickname.toString(), + context.locale), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -767,7 +792,8 @@ class _PostViewPageState extends State { onTap: () async { // 자신의 글에는 요청을 보내지 않고 미리 차단하기 if (_article.is_mine) { - showInfoBySnackBar(context, "본인 게시글이나 댓글에는 좋아요를 누를 수 없습니다."); + showInfoBySnackBar( + context, LocaleKeys.postViewPage_noSelfVotingInfo.tr()); return; } // 다른 사람의 글인 경우 @@ -782,7 +808,7 @@ class _PostViewPageState extends State { model: tmpArticle, userProvider: userProvider, ).posVote(); - debugPrint('좋아요 결과 ${res}'); + debugPrint('좋아요 결과 $res'); if (!res) { ArticleController(model: _article, userProvider: userProvider) .setVote(true); @@ -808,7 +834,8 @@ class _PostViewPageState extends State { // TODO: onTap 메서드 함수화하기 onTap: () async { if (_article.is_mine) { - showInfoBySnackBar(context, "본인 게시글이나 댓글에는 좋아요를 누를 수 없습니다."); + showInfoBySnackBar( + context, LocaleKeys.postViewPage_noSelfVotingInfo.tr()); return; } else { ArticleModel tmpArticle = @@ -820,7 +847,7 @@ class _PostViewPageState extends State { model: tmpArticle, userProvider: userProvider, ).negVote(); - debugPrint('싫어요 결과 ${res}'); + debugPrint('싫어요 결과 $res'); if (!res) { ArticleController(model: _article, userProvider: userProvider) .setVote(false); @@ -895,7 +922,7 @@ class _PostViewPageState extends State { }); }, child: Container( - width: 88, + width: context.locale == const Locale('ko') ? 80 : 110, height: 35, decoration: BoxDecoration( color: Colors.white, @@ -922,7 +949,9 @@ class _PostViewPageState extends State { ), //const SizedBox(width: 4), Text( - _article.my_scrap == null ? '담아두기' : '담아둔 글', + _article.my_scrap == null + ? LocaleKeys.postViewPage_scrap.tr() + : LocaleKeys.postViewPage_scrapped.tr(), style: TextStyle( color: _article.my_scrap == null ? const Color(0xFF646464) @@ -957,12 +986,13 @@ class _PostViewPageState extends State { height: 32, ), const SizedBox(width: 8), - const Flexible( + Flexible( child: Text( - "URL을 클립 보드에 복사했습니다.", + LocaleKeys.postViewPage_copyLinkToClipBoard + .tr(), // 오버플로우 나면 다음줄로 넘어가도록 하기 위해 overflow: TextOverflow.visible, - style: TextStyle( + style: const TextStyle( color: Colors.black, fontWeight: FontWeight.w400, fontSize: 15, @@ -995,9 +1025,9 @@ class _PostViewPageState extends State { Color.fromRGBO(100, 100, 100, 1), BlendMode.srcIn), ), //const SizedBox(width: 6), - const Text( - '공유', - style: TextStyle( + Text( + LocaleKeys.postViewPage_share.tr(), + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF646464), @@ -1065,7 +1095,8 @@ class _PostViewPageState extends State { else { debugPrint("failed to block"); Navigator.pop(context); - showInfoBySnackBar(context, "차단에 실패했습니다."); + showInfoBySnackBar(context, + LocaleKeys.postViewPage_failedToBlock.tr()); } }); }, @@ -1123,8 +1154,8 @@ class _PostViewPageState extends State { blockedProvider.blockedAnonymousPostIDs .contains(_article.created_by.id .toString()))) - ? '차단 해제' - : '차단', + ? LocaleKeys.postViewPage_unblock.tr() + : LocaleKeys.postViewPage_block.tr(), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, @@ -1183,9 +1214,9 @@ class _PostViewPageState extends State { height: 22, colorFilter: const ColorFilter.mode( Color(0xFF646464), BlendMode.srcIn)), - const Text( - '삭제', - style: TextStyle( + Text( + LocaleKeys.postViewPage_delete.tr(), + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF646464)), @@ -1225,9 +1256,9 @@ class _PostViewPageState extends State { colorFilter: const ColorFilter.mode( Color(0xFF646464), BlendMode.srcIn), ), - const Text( - '신고', - style: TextStyle( + Text( + LocaleKeys.postViewPage_report.tr(), + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF646464)), @@ -1268,9 +1299,9 @@ class _PostViewPageState extends State { BlendMode.srcIn, ), ), - const Text( - '수정', - style: TextStyle( + Text( + LocaleKeys.postViewPage_edit.tr(), + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF646464)), @@ -1351,8 +1382,11 @@ class _PostViewPageState extends State { MediaQuery.of(context).size.width - 250, ), child: Text( - curComment.created_by.profile.nickname - .toString(), + getName( + curComment.name_type, + curComment.created_by.profile.nickname + .toString(), + context.locale), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -1361,7 +1395,7 @@ class _PostViewPageState extends State { overflow: TextOverflow.ellipsis, )), const SizedBox(width: 7), - Text(getTime(curComment.created_at), + Text(getTime(curComment.created_at, context.locale), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w400, @@ -1432,9 +1466,9 @@ class _PostViewPageState extends State { child: (_isAnonymousIOS(_article) && blockedProvider.blockedAnonymousPostIDs .contains(curComment.created_by.id.toString())) - ? const Text( - "차단한 사용자의 댓글입니다.", - style: TextStyle( + ? Text( + LocaleKeys.postViewPage_blockedUsersComment.tr(), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w400, color: Colors.grey, @@ -1443,7 +1477,8 @@ class _PostViewPageState extends State { : (curComment.is_hidden == false ? _buildCommentContent(curComment.content ?? "") : Text( - getHiddenCommentReasons(curComment.why_hidden), + getHiddenCommentReasons( + curComment.why_hidden, context.locale), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w400, @@ -1465,8 +1500,10 @@ class _PostViewPageState extends State { // onTap 메서드 함수화하기 onTap: () async { if (curComment.is_mine) { - showInfoBySnackBar(context, - "본인 게시글이나 댓글에는 좋아요를 누를 수 없습니다."); + showInfoBySnackBar( + context, + LocaleKeys.postViewPage_noSelfVotingInfo + .tr()); return; } else { CommentNestedCommentListActionModel @@ -1482,7 +1519,7 @@ class _PostViewPageState extends State { model: tmpCurComment, userProvider: userProvider, ).posVote(); - debugPrint('좋아요 결과 ${res}'); + debugPrint('좋아요 결과 $res'); if (!res) { CommentController( model: curComment, @@ -1512,8 +1549,10 @@ class _PostViewPageState extends State { onTap: () async { // onTap 메서드 함수화하기 if (curComment.is_mine) { - showInfoBySnackBar(context, - "본인 게시글이나 댓글에는 좋아요를 누를 수 없습니다."); + showInfoBySnackBar( + context, + LocaleKeys.postViewPage_noSelfVotingInfo + .tr()); return; } else { // 원래 모델값을 저장하기 위해 임시 모델 생성 @@ -1530,7 +1569,7 @@ class _PostViewPageState extends State { model: tmpCurComment, userProvider: userProvider, ).negVote(); - debugPrint('좋아요 결과 ${res}'); + debugPrint('좋아요 결과 $res'); if (!res) { CommentController( model: curComment, @@ -1580,9 +1619,9 @@ class _PostViewPageState extends State { width: 11, height: 19, ), - const Text( - '답글 쓰기', - style: TextStyle( + Text( + LocaleKeys.postViewPage_reply.tr(), + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, ), @@ -1641,6 +1680,7 @@ class _PostViewPageState extends State { width: 1.0, color: Color(0xFFF0F0F0)), // 원하는 색상과 두께로 설정 ), ), + padding: const EdgeInsets.only(top: 7), child: Column( key: _textFieldKey, crossAxisAlignment: CrossAxisAlignment.start, @@ -1651,7 +1691,9 @@ class _PostViewPageState extends State { children: [ const SizedBox(height: 3), Text( - '${(targetComment == null ? false : targetComment!.is_mine) ? '\'나\'에게' : "'${targetComment?.created_by.profile.nickname}'님께"} 답글을 작성하는 중', + context.locale == const Locale('ko') + ? '${(targetComment == null ? false : targetComment!.is_mine) ? "'나'에게" : "'${targetComment?.created_by.profile.nickname}'님께"} 답글을 작성하는 중' + : "Replying to '${targetComment?.created_by.profile.nickname}'", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -1669,7 +1711,9 @@ class _PostViewPageState extends State { children: [ const SizedBox(height: 3), Text( - '나의 댓글 "${targetComment?.content}" 수정 중', + context.locale == const Locale('ko') + ? '나의 댓글 "${targetComment?.content}" 수정 중' + : 'Editing my comment "${targetComment?.content}"', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -1715,7 +1759,6 @@ class _PostViewPageState extends State { // TextFormField Expanded( child: Container( - margin: const EdgeInsets.only(top: 7), constraints: const BoxConstraints( minHeight: 36, ), @@ -1732,6 +1775,23 @@ class _PostViewPageState extends State { absorbing: _isSending, child: InkWell( onTap: () async { + debugPrint("post_view_page.dart: Send Comment"); + if (Platform.isAndroid && + (userProvider.naUser!.email != null && + userProvider.naUser!.email == + "tkddh1109@gmail.com")) { + await showDialog( + builder: (context) => ForAndroidTesterDialog( + userProvider: userProvider, + targetContext: context, + onTap: () { + Navigator.pop(context); + }, + ), + context: context); + + return; + } _setIsSending(true); bool sendRes = await _sendComment(userProvider); if (sendRes) { @@ -1774,9 +1834,9 @@ class _PostViewPageState extends State { return Form( key: _formKey, child: Container( - margin: const EdgeInsets.only(left: 15), + margin: const EdgeInsets.only(left: 13), child: TextFormField( - style: TextStyle( + style: const TextStyle( fontSize: 14, ), cursorColor: ColorsInfo.newara, @@ -1785,10 +1845,10 @@ class _PostViewPageState extends State { minLines: 1, maxLines: 5, keyboardType: TextInputType.multiline, - decoration: const InputDecoration( + decoration: InputDecoration( border: InputBorder.none, - hintText: '댓글을 입력해주세요', - hintStyle: TextStyle( + hintText: LocaleKeys.postViewPage_commentHintText.tr(), + hintStyle: const TextStyle( color: Color(0xFFBBBBBB), fontSize: 14, fontWeight: FontWeight.w500, @@ -1796,7 +1856,7 @@ class _PostViewPageState extends State { ), validator: (value) { if (value == null || value.isEmpty) { - return '댓글이 작성되지 않았습니다!'; + return LocaleKeys.postViewPage_noCommentWarning.tr(); } return null; }, @@ -1887,12 +1947,9 @@ class _PostViewPageState extends State { defaultPayload.addAll(targetComment != null ? {"parent_comment": targetComment!.id} : {"parent_article": _article.id}); - var postRes = await userProvider.postApiRes( - 'comments/', - payload: defaultPayload, - ); - if (postRes.statusCode != 201) { - // 나중에 사용자용 알림 기능 추가해야 함 + var postRes = + await userProvider.postApiRes('comments/', data: defaultPayload); + if (postRes == null) { debugPrint("POST /api/comments failed"); return false; } @@ -1907,9 +1964,9 @@ class _PostViewPageState extends State { }; Response? patchRes = await userProvider.patchApiRes( "comments/${targetComment!.id}/", - payload: defaultPayload, + data: defaultPayload, ); - if (patchRes != null && patchRes.statusCode != 200) { + if (patchRes == null) { debugPrint("PATCH /api/comments/${targetComment!.id}/ failed"); return false; } @@ -1950,7 +2007,7 @@ class _PostViewPageState extends State { // TODO: 아래 코드는 iOS 심사 통과를 위한 임시 방편. 익명 차단이 BE에서 구현되면 제거해야함 (2023.02.29) /// 익명게시글에 차단 버튼을 표시해야하는지 여부를 나타냄(iOS 리젝을 피하기 위해 iOS 환경에서 익명 게시글일 때만 true 반환) - bool _isAnonymousIOS(_article) { - return (Platform.isIOS && _article.name_type == 2); + bool _isAnonymousIOS(ArticleModel article) { + return (Platform.isIOS && article.name_type == 2); } } diff --git a/lib/pages/post_write_page.dart b/lib/pages/post_write_page.dart index c4aeb2d9..e85b9f17 100644 --- a/lib/pages/post_write_page.dart +++ b/lib/pages/post_write_page.dart @@ -17,11 +17,10 @@ import 'package:new_ara_app/models/attachment_model.dart'; import 'package:new_ara_app/models/board_detail_action_model.dart'; import 'package:new_ara_app/models/simple_board_model.dart'; import 'package:new_ara_app/models/topic_model.dart'; -import 'package:new_ara_app/pages/bulletin_search_page.dart'; import 'package:new_ara_app/pages/post_view_page.dart'; import 'package:new_ara_app/pages/terms_and_conditions_page.dart'; import 'package:new_ara_app/providers/user_provider.dart'; -import 'package:new_ara_app/utils/create_dio_with_config.dart'; +import 'package:new_ara_app/utils/cache_function.dart'; import 'package:new_ara_app/utils/slide_routing.dart'; import 'package:new_ara_app/widgets/loading_indicator.dart'; import 'package:provider/provider.dart'; @@ -37,6 +36,8 @@ import 'package:html2md/html2md.dart' as html2md; import 'package:markdown_quill/markdown_quill.dart'; import 'package:markdown/markdown.dart' as md; import 'package:new_ara_app/widgets/snackbar_noti.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; /// 사용자가 게시물을 작성하거나 편집할 수 있는 페이지를 나타내는 StatefulWidget입니다. class PostWritePage extends StatefulWidget { @@ -47,8 +48,7 @@ class PostWritePage extends StatefulWidget { final BoardDetailActionModel? previousBoard; /// 생성자에서 이전 게시물의 데이터를 선택적으로 받을 수 있습니다. - const PostWritePage({Key? key, this.previousArticle, this.previousBoard}) - : super(key: key); + const PostWritePage({super.key, this.previousArticle, this.previousBoard}); @override State createState() => _PostWritePageState(); @@ -113,7 +113,7 @@ class _PostWritePageState extends State final _defaultTopicModelSelect = TopicModel( id: -1, slug: "", - ko_name: "말머리를 선택하세요", + ko_name: LocaleKeys.postWritePage_selectCategory.tr(), en_name: "No Topic", ); final _defaultBoardDetailActionModel = BoardDetailActionModel( @@ -122,7 +122,7 @@ class _PostWritePageState extends State user_readable: true, user_writable: true, slug: '', - ko_name: '게시판을 선택하세요', + ko_name: LocaleKeys.postWritePage_selectBoard.tr(), en_name: 'No Board', group: SimpleBoardModel(id: -1, slug: '', ko_name: '', en_name: ''), ); @@ -175,7 +175,7 @@ class _PostWritePageState extends State bool _isKeyboardClosed = true; - var _editorScrollController = ScrollController(); + final _editorScrollController = ScrollController(); @override void initState() { super.initState(); @@ -269,43 +269,35 @@ class _PostWritePageState extends State /// API에서 게시판 목록을 가져와 `_boardList`에 저장. Future _getBoardList() async { // 사용자 정보 제공자로부터 쿠키 정보 가져오기. - var userProvider = context.read(); - try { - Dio dio = userProvider.createDioWithHeadersForGet(); - var response = await dio.get('$newAraDefaultUrl/api/boards/'); - - // 기본 게시판 정보를 `_boardList`에 초기화. - _boardList = [_defaultBoardDetailActionModel]; - - // API 응답으로부터 게시판 목록 파싱 후 `_boardList`에 추가. - for (Map json in response.data) { - try { - BoardDetailActionModel boardDetail = - BoardDetailActionModel.fromJson(json); - if (boardDetail.user_writable) { - _boardList.add(boardDetail); + UserProvider userProvider = context.read(); + + //게시판 목록을 API에서 혹은 cache에서 불러오기. + await updateStateWithCachedOrFetchedApiData( + apiUrl: "boards/", // API URL을 지정합니다. 이 예에서는 "boards/"를 대상으로 합니다. + userProvider: userProvider, // API 요청을 담당할 userProvider 인스턴스를 전달합니다. + callback: (response) { + if (mounted) { + setState(() { + // `_boardList` 초기화 후 기본 게시판 추가. + _boardList = [_defaultBoardDetailActionModel]; + + // Json 응답에서 게시물 목록 파싱 후 `_boardList`에 추가. + for (Map boardJson in response) { + try { + BoardDetailActionModel boardDetail = + BoardDetailActionModel.fromJson(boardJson); + if (boardDetail.user_writable) { + _boardList.add(boardDetail); + } + } catch (error) { + debugPrint( + "refreshBoardList BoardDetailActionModel.fromJson 실패: $error"); + return; + } + } + }); } - } catch (error) { - debugPrint( - "refreshBoardList BoardDetailActionModel.fromJson 실패: $error"); - return; - } - } - } on DioException catch (e) { - if (e.response != null) { - // 응답이 있는 에러 - debugPrint('Dio error!'); - debugPrint('STATUS: ${e.response?.statusCode}'); - debugPrint('DATA: ${e.response?.data}'); - debugPrint('HEADERS: ${e.response?.headers}'); - } else { - // 응답이 없는 에러 - debugPrint('Error sending request!'); - debugPrint(e.message); - } - } catch (error) { - return; - } + }); // 게시판 목록 상태 업데이트.(넘어본 게시판의 정보가 있을 경우 && 게시물을 쓸 수 있는 게시판의 경우) if (widget.previousBoard != null && widget.previousBoard!.user_writable) { @@ -419,6 +411,8 @@ class _PostWritePageState extends State @override Widget build(BuildContext context) { + debugPrint("BUILD invoked!!!"); + /// 게시물 업로드 가능한지 확인 /// TODO: 업로드 로딩 인디케이터 추가하기 bool canIupload = _titleController.text != '' && @@ -477,10 +471,10 @@ class _PostWritePageState extends State height: 35), onPressed: () => Navigator.pop(context), ), - title: const SizedBox( + title: SizedBox( child: Text( - "글 쓰기", - style: TextStyle( + LocaleKeys.postWritePage_write.tr(), + style: const TextStyle( color: ColorsInfo.newara, fontSize: 18, fontWeight: FontWeight.w700, @@ -497,8 +491,8 @@ class _PostWritePageState extends State isUpdate: true, previousArticleId: widget.previousArticle!.id) : _managePost()) - : () => - showInfoBySnackBar(context, "게시판을 선택해주시고 제목, 내용을 입력해주세요."), + : () => showInfoBySnackBar( + context, LocaleKeys.postWritePage_conditionSnackBar.tr()), // 버튼이 클릭되었을 때 수행할 동작 padding: EdgeInsets.zero, // 패딩 제거 child: canIupload @@ -511,9 +505,9 @@ class _PostWritePageState extends State constraints: const BoxConstraints(maxWidth: 65.0, maxHeight: 35.0), alignment: Alignment.center, - child: const Text( - '올리기', - style: TextStyle(color: Colors.white), + child: Text( + LocaleKeys.postWritePage_submit.tr(), + style: const TextStyle(color: Colors.white), ), ), ) @@ -529,9 +523,9 @@ class _PostWritePageState extends State constraints: const BoxConstraints(maxWidth: 65.0, maxHeight: 35.0), alignment: Alignment.center, - child: const Text( - '올리기', - style: TextStyle(color: Color(0xFFBBBBBB)), + child: Text( + LocaleKeys.postWritePage_submit.tr(), + style: const TextStyle(color: Color(0xFFBBBBBB)), ), ), ), @@ -567,7 +561,7 @@ class _PostWritePageState extends State // TODO: 원하는 메뉴 모양 만들기 위해 속성 테스트 할 것 // isDense: true, // isExpanded: true, - + isExpanded: true, value: _chosenBoardValue, style: const TextStyle(color: ColorsInfo.newara), borderRadius: BorderRadius.circular(20.0), @@ -580,7 +574,9 @@ class _PostWritePageState extends State child: Padding( padding: const EdgeInsets.only(left: 0.0), child: Text( - value.ko_name, + context.locale == const Locale('ko') + ? value.ko_name + : value.en_name, style: TextStyle( color: value.id == -1 || _isEditingPost ? const Color(0xFFBBBBBB) @@ -616,6 +612,7 @@ class _PostWritePageState extends State child: ButtonTheme( alignedDropdown: true, child: DropdownButton( + isExpanded: true, value: _chosenTopicValue, style: const TextStyle(color: Colors.red), borderRadius: BorderRadius.circular(20.0), @@ -624,7 +621,9 @@ class _PostWritePageState extends State return DropdownMenuItem( value: value, child: Text( - value.ko_name, + context.locale == const Locale('ko') + ? value.ko_name + : value.en_name, style: TextStyle( color: value.id == -1 || _isEditingPost ? const Color(0xFFBBBBBB) @@ -674,9 +673,9 @@ class _PostWritePageState extends State // build 함수를 다시 실행하여 올릴 수 있는 게시물인지 유효성 검사 setState(() {}); }, - decoration: const InputDecoration( - hintText: "제목을 입력해주세요.", - hintStyle: TextStyle( + decoration: InputDecoration( + hintText: LocaleKeys.postWritePage_titleHintText.tr(), + hintStyle: const TextStyle( height: 27 / 22, fontSize: 22, color: Color(0xFFBBBBBB), @@ -689,13 +688,13 @@ class _PostWritePageState extends State fillColor: Colors.white, isDense: true, isCollapsed: true, - contentPadding: EdgeInsets.fromLTRB(0, 0, 0, 0), - enabledBorder: OutlineInputBorder( + contentPadding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + enabledBorder: const OutlineInputBorder( borderSide: BorderSide( color: Colors.transparent, // 테두리 색상 설정 ), // 모서리를 둥글게 설정 ), - focusedBorder: OutlineInputBorder( + focusedBorder: const OutlineInputBorder( borderSide: BorderSide( color: Colors.transparent, // 테두리 색상 설정 ), // 모서리를 둥글게 설정 @@ -725,9 +724,9 @@ class _PostWritePageState extends State Builder( builder: (BuildContext context) { if (_chosenBoardValue == _defaultBoardDetailActionModel) { - return const Text( - "게시판을 선택해주세요.", - style: TextStyle( + return Text( + LocaleKeys.postWritePage_selectBoard.tr(), + style: const TextStyle( fontSize: 16, height: 24 / 16, fontWeight: FontWeight.w500, @@ -803,9 +802,9 @@ class _PostWritePageState extends State width: 34, height: 34, ), - const Text( - "첨부파일 추가", - style: TextStyle( + Text( + LocaleKeys.postWritePage_addAttach.tr(), + style: const TextStyle( fontWeight: FontWeight.w500, fontSize: 16, color: Color(0xFF636363)), @@ -855,9 +854,9 @@ class _PostWritePageState extends State }, child: Row( children: [ - const Text( - "첨부파일", - style: TextStyle( + Text( + LocaleKeys.postWritePage_attachments.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), @@ -1040,8 +1039,7 @@ class _PostWritePageState extends State const SizedBox( width: 20, ), - _buildCheckBox(), - const Spacer(), + Expanded(child: _buildCheckBox()), GestureDetector( onTap: () { Navigator.of(context).push( @@ -1050,9 +1048,9 @@ class _PostWritePageState extends State ), ); }, - child: const Text( - "이용약관", - style: TextStyle( + child: Text( + LocaleKeys.postWritePage_terms.tr(), + style: const TextStyle( decoration: TextDecoration.underline, fontSize: 16, fontWeight: FontWeight.w500, @@ -1075,8 +1073,9 @@ class _PostWritePageState extends State Widget _buildCheckBox() { if (_chosenBoardValue != null && _chosenBoardValue!.slug == 'with-school') { - return const Text("이 게시물은 실명으로 게시됩니다.", - style: TextStyle( + return Text(LocaleKeys.postWritePage_realNameNotice.tr(), + style: const TextStyle( + overflow: TextOverflow.ellipsis, fontSize: 16, fontWeight: FontWeight.w500, color: ColorsInfo.newara, @@ -1119,7 +1118,7 @@ class _PostWritePageState extends State width: 6, ), Text( - "익명", + LocaleKeys.postWritePage_anonymous.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -1169,7 +1168,7 @@ class _PostWritePageState extends State width: 6, ), Text( - "성인", + LocaleKeys.postWritePage_adult.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -1217,7 +1216,7 @@ class _PostWritePageState extends State width: 6, ), Text( - "정치", + LocaleKeys.postWritePage_politics.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -1278,7 +1277,6 @@ class _PostWritePageState extends State TextStyle h1h2h3h4h5h6CommonStyle = const TextStyle( color: Colors.black, fontWeight: FontWeight.w600, - fontFamily: 'NotoSansKR', height: 1.15, ); return quill.DefaultStyles( @@ -1312,7 +1310,6 @@ class _PostWritePageState extends State const TextStyle( color: Color(0xFF4a4a4a), fontWeight: FontWeight.w500, - fontFamily: 'NotoSansKR', height: 1.5, fontSize: 16, ), @@ -1322,15 +1319,11 @@ class _PostWritePageState extends State ), bold: const TextStyle( - color: Color(0xff363636), - fontFamily: 'NotoSansKR', - fontWeight: FontWeight.w700), + color: Color(0xff363636), fontWeight: FontWeight.w700), italic: const TextStyle( - fontFamily: 'NotoSansKR', fontStyle: FontStyle.italic, ), underline: const TextStyle( - fontFamily: 'NatoSansKR', decoration: TextDecoration.underline, ), // 태그 @@ -1338,7 +1331,6 @@ class _PostWritePageState extends State style: const TextStyle( color: Color(0xffff3860), fontWeight: FontWeight.w400, - fontFamily: 'NotoSansKR', height: 1.5, fontSize: 14, ), @@ -1350,7 +1342,6 @@ class _PostWritePageState extends State const TextStyle( color: Color(0xffBBBBBB), fontWeight: FontWeight.w500, - fontFamily: 'NotoSansKR', height: 1.5, fontSize: 16, ), @@ -1364,7 +1355,6 @@ class _PostWritePageState extends State // backgroundColor: Colors.grey, color: Color(0xFF4a4a4a), fontWeight: FontWeight.w400, - fontFamily: 'NotoSansKR', height: 1.5, fontSize: 16, ), @@ -1385,7 +1375,7 @@ class _PostWritePageState extends State child: quill.QuillEditor( focusNode: _editorFocusNode, controller: _quillController, - placeholder: '내용을 입력해주세요.', + placeholder: LocaleKeys.postWritePage_contentPlaceholder.tr(), embedBuilders: FlutterQuillEmbeds.builders(), readOnly: false, // The editor is editable @@ -1529,7 +1519,8 @@ class _PostWritePageState extends State _isLoading = true; }); - Dio dio = userProvider.createDioWithHeadersForNonget(); + Dio dio = userProvider + .createDioWithHeadersForNonget(); // TODO: 적절한 apiRes함수로 변경해야 함. for (int i = 0; i < _attachmentList.length; i++) { //새로 올리는 파일이면 새로운 id 할당 받기. @@ -1541,28 +1532,16 @@ class _PostWritePageState extends State "file": await MultipartFile.fromFile(attachFile.path, filename: attachFile.path.split('/').last), }); - try { - Response response = await dio - .post("$newAraDefaultUrl/api/attachments/", data: formData); + Response? response = await userProvider.postApiRes( + "$newAraDefaultUrl/api/attachments/", + data: formData); + if (response != null) { final attachmentModel = AttachmentModel.fromJson(response.data); attachmentIds.add(attachmentModel.id); contentValue = _manageImgTagSrc(contentValue, _attachmentList[i].fileLocalPath!, attachmentModel.file); - } on DioException catch (e) { - // Handle the DioError separately to handle only Dio related errors - if (e.response != null) { - // DioError contains response data - debugPrint('Dio error!'); - debugPrint('STATUS: ${e.response?.statusCode}'); - debugPrint('DATA: ${e.response?.data}'); - debugPrint('HEADERS: ${e.response?.headers}'); - } else { - // Error due to setting up or sending/receiving the request - debugPrint('Error sending request!'); - debugPrint(e.message); - } - } catch (error) { - debugPrint("$error"); + } else { + debugPrint("POST /api/attachments/ failed"); } } } else { @@ -1571,38 +1550,37 @@ class _PostWritePageState extends State } } - try { - Response response; - var data = { - 'title': titleValue, - 'content': contentValue, - 'attachments': attachmentIds, - 'is_content_sexual': _selectedCheckboxes[1], - 'is_content_social': _selectedCheckboxes[2], - // TODO: 명명 규칙 다름 - 'name_type': _chosenBoardValue!.slug == 'with-school' - ? 'REALNAME' - : _chosenBoardValue!.slug == "talk" && - _selectedCheckboxes[0]! == true - ? 'ANONYMOUS' - : 'REGULAR', - }; - - if (isUpdate) { - response = await dio.put( - '$newAraDefaultUrl/api/articles/$previousArticleId/', - data: data, - ); - } else { - data['parent_topic'] = - _chosenTopicValue!.id == -1 ? '' : _chosenTopicValue!.id; - data['parent_board'] = _chosenBoardValue!.id; - response = await dio.post( - '$newAraDefaultUrl/api/articles/', - data: data, - ); - } - + // TODO: putApiRes 만들고 나서 try-catch 한번에 제거하기. + var data = { + 'title': titleValue, + 'content': contentValue, + 'attachments': attachmentIds, + 'is_content_sexual': _selectedCheckboxes[1], + 'is_content_social': _selectedCheckboxes[2], + // TODO: 명명 규칙 다름 + 'name_type': _chosenBoardValue!.slug == 'with-school' + ? 'REALNAME' + : _chosenBoardValue!.slug == "talk" && + _selectedCheckboxes[0]! == true + ? 'ANONYMOUS' + : 'REGULAR', + }; + Response? response; + if (isUpdate) { + response = await userProvider.putApiRes( + 'articles/$previousArticleId/', + data: data, + ); + } else { + data['parent_topic'] = + _chosenTopicValue!.id == -1 ? '' : _chosenTopicValue!.id; + data['parent_board'] = _chosenBoardValue!.id; + response = await userProvider.postApiRes( + 'articles/', + data: data, + ); + } + if (response != null) { debugPrint('Response data: ${response.data}'); if (mounted) { if (_isEditingPost) { @@ -1617,8 +1595,9 @@ class _PostWritePageState extends State ); } } - } on DioException catch (error) { - debugPrint('post Error: ${error.response!.data}'); + } + else { + debugPrint("post Error occurred"); } if (mounted) { diff --git a/lib/pages/profile_edit_page.dart b/lib/pages/profile_edit_page.dart index 3414a879..d83b1e49 100644 --- a/lib/pages/profile_edit_page.dart +++ b/lib/pages/profile_edit_page.dart @@ -2,7 +2,8 @@ import 'dart:io'; import 'package:http_parser/http_parser.dart'; import 'package:flutter/material.dart'; import 'package:new_ara_app/constants/url_info.dart'; -import 'package:new_ara_app/utils/create_dio_with_config.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:provider/provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; @@ -16,7 +17,6 @@ import 'package:new_ara_app/widgets/loading_indicator.dart'; import 'package:new_ara_app/providers/notification_provider.dart'; import 'package:new_ara_app/utils/profile_image.dart'; import 'package:new_ara_app/widgets/snackbar_noti.dart'; -import 'package:new_ara_app/widgets/text_info.dart'; class ProfileEditPage extends StatefulWidget { const ProfileEditPage({super.key}); @@ -30,7 +30,10 @@ class _ProfileEditPageState extends State { final ImagePicker _imagePicker = ImagePicker(); bool _isLoading = false, _isCamClicked = false; XFile? _selectedImage; - String? _changedNick, _retrieveDataError; + String? _changedNick; + + // ignore: unused_field + String? _retrieveDataError; @override void initState() { @@ -104,37 +107,44 @@ class _ProfileEditPageState extends State { ); } var formData = FormData.fromMap(payload); - Dio dio = userProvider.createDioWithHeadersForNonget(); - try { - var response = await dio.patch( - "$newAraDefaultUrl/api/user_profiles/${userProfileModel.user}/", - data: formData, - ); - if (response.statusCode != 200) return false; - } on DioException catch (e) { - debugPrint("updateProfile failed with DioException: $e"); - // 서버에서 response를 보냈지만 invalid한 statusCode일 때 - String infoText = '설정 변경 중 문제가 발생했습니다.'; - if (e.response != null) { - debugPrint("${e.response!.data['nickname'][0]}"); - debugPrint("${e.response!.headers}"); - debugPrint("${e.response!.requestOptions}"); - // 인터넷 문제가 아닌 경우 닉네임 관련 규정 설명을 추가함. - infoText += ' ${e.response!.data['nickname'][0]}'; - } - // request의 setting, sending에서 문제 발생 - // requestOption, message를 출력. - else { - debugPrint("${e.requestOptions}"); - debugPrint("${e.message}"); - } - // 유저에게 스낵바 알림 - noticeUserBySnackBar(infoText); - return false; - } catch (e) { - debugPrint("updateProfile failed with error: $e"); + Response? response = await userProvider.patchApiRes( + "user_profiles/${userProfileModel.user}/", + data: formData, + ); + if (response == null) { + // TODO: 닉네임 변경 기한 관련 메시지 출력 기능 추가 (인터넷 에러 처리할 때 완료하기) return false; } + // try { + // var response = await dio.patch( + // "$newAraDefaultUrl/api/user_profiles/${userProfileModel.user}/", + // data: formData, + // ); + // if (response.statusCode != 200) return false; + // } on DioException catch (e) { + // debugPrint("updateProfile failed with DioException: $e"); + // // 서버에서 response를 보냈지만 invalid한 statusCode일 때 + // String infoText = LocaleKeys.profileEditPage_settingInfoText.tr(); + // if (e.response != null) { + // debugPrint("${e.response!.data['nickname'][0]}"); + // debugPrint("${e.response!.headers}"); + // debugPrint("${e.response!.requestOptions}"); + // // 인터넷 문제가 아닌 경우 닉네임 관련 규정 설명을 추가함. + // infoText += ' ${e.response!.data['nickname'][0]}'; + // } + // // request의 setting, sending에서 문제 발생 + // // requestOption, message를 출력. + // else { + // debugPrint("${e.requestOptions}"); + // debugPrint("${e.message}"); + // } + // // 유저에게 스낵바 알림 + // noticeUserBySnackBar(infoText); + // return false; + // } catch (e) { + // debugPrint("updateProfile failed with error: $e"); + // return false; + // } return true; } @@ -199,17 +209,17 @@ class _ProfileEditPageState extends State { ], ), ), - title: const Text( - "프로필 수정", - style: TextStyle( + title: Text( + LocaleKeys.profileEditPage_editProfile.tr(), + style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 18, color: ColorsInfo.newara, ), ), actions: [ - IconButton( - onPressed: () async { + InkWell( + onTap: () async { _setIsLoading(true); bool updateRes = await _updateProfile(userProvider); debugPrint("updateRes: $updateRes"); @@ -223,15 +233,21 @@ class _ProfileEditPageState extends State { _setIsLoading(false); } }, - icon: const Text( - '완료', - style: TextStyle( - color: ColorsInfo.newara, - fontWeight: FontWeight.w500, - fontSize: 17, + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Text( + LocaleKeys.profileEditPage_complete.tr(), + style: const TextStyle( + color: ColorsInfo.newara, + fontWeight: FontWeight.w500, + fontSize: 17, + ), + ), ), ), ), + const SizedBox(width: 10), ], ), body: SafeArea( @@ -293,7 +309,8 @@ class _ProfileEditPageState extends State { ), child: SvgPicture.asset( "assets/icons/camera.svg", - color: Colors.white, + colorFilter: const ColorFilter.mode( + Colors.white, BlendMode.srcIn), ), ), ), @@ -306,9 +323,9 @@ class _ProfileEditPageState extends State { width: mediaQueryData.size.width - 60, child: Row( children: [ - const Text( - '닉네임', - style: TextStyle( + Text( + LocaleKeys.profileEditPage_nickname.tr(), + style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 17, color: Color.fromRGBO(99, 99, 99, 1), @@ -332,11 +349,14 @@ class _ProfileEditPageState extends State { // 닉네임 정책 안내 문구 SizedBox( width: mediaQueryData.size.width - 60, - child: const Padding( - padding: EdgeInsets.only(left: 80), + child: Padding( + padding: EdgeInsets.only( + left: context.locale == const Locale('ko') + ? 80 + : 110), child: Text( - '닉네임은 한번 변경할 시 3개월간 변경이 불가합니다.', - style: TextStyle( + LocaleKeys.profileEditPage_nicknameInfo.tr(), + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color.fromRGBO(191, 191, 191, 1), @@ -349,19 +369,22 @@ class _ProfileEditPageState extends State { width: mediaQueryData.size.width - 60, child: Row( children: [ - const Text( - '이메일', - style: TextStyle( + Text( + LocaleKeys.profileEditPage_email.tr(), + style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 17, color: Color.fromRGBO(99, 99, 99, 1), ), ), - const SizedBox(width: 45), + SizedBox( + width: context.locale == const Locale('ko') + ? 45 + : 80), Expanded( child: Text( userProviderData.naUser!.email ?? - "이메일 정보가 없습니다.", + LocaleKeys.profileEditPage_noEmail.tr(), style: const TextStyle( fontWeight: FontWeight.w500, fontSize: 15, @@ -411,14 +434,14 @@ class _ProfileEditPageState extends State { initialValue: initialNick, maxLines: 1, keyboardType: TextInputType.multiline, - decoration: const InputDecoration( + decoration: InputDecoration( border: InputBorder.none, - hintText: '변경하실 닉네임을 입력해주세요.', + hintText: LocaleKeys.profileEditPage_nicknameHintText.tr(), ), validator: (value) { // (2023.08.19) 나중에 글자 수 확인도 추가해야 함 if (value == null || value.isEmpty) { - return '닉네임이 작성되지 않았습니다!'; + return LocaleKeys.profileEditPage_nicknameEmptyInfo.tr(); } return null; }, diff --git a/lib/pages/setting_page.dart b/lib/pages/setting_page.dart index 1160b4ae..e2eaec89 100644 --- a/lib/pages/setting_page.dart +++ b/lib/pages/setting_page.dart @@ -1,7 +1,5 @@ -/// 유저 설정 관리, 차단한 유저 목록, 로그아웃을 관리하는 파일. -/// Author: 김상오(alvin) - -import 'dart:convert'; +// 유저 설정 관리, 차단한 유저 목록, 로그아웃을 관리하는 파일. +// Author: 김상오(alvin) import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -10,7 +8,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:new_ara_app/constants/url_info.dart'; import 'package:new_ara_app/pages/terms_and_conditions_page.dart'; import 'package:new_ara_app/providers/user_provider.dart'; -import 'package:new_ara_app/utils/create_dio_with_config.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/utils/slide_routing.dart'; import 'package:new_ara_app/widgets/loading_indicator.dart'; import 'package:provider/provider.dart'; @@ -22,16 +20,13 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:new_ara_app/constants/colors_info.dart'; import 'package:new_ara_app/widgets/text_info.dart'; -import 'package:new_ara_app/widgets/border_boxes.dart'; -import 'package:new_ara_app/widgets/text_and_switch.dart'; import 'package:new_ara_app/providers/notification_provider.dart'; -import 'package:new_ara_app/models/block_model.dart'; import 'package:new_ara_app/widgets/dialogs.dart'; import 'package:new_ara_app/widgets/snackbar_noti.dart'; /// 설정 페이지 빌드 및 이벤트 처리를 담당하는 StatefulWidget. class SettingPage extends StatefulWidget { - const SettingPage({Key? key}) : super(key: key); + const SettingPage({super.key}); @override State createState() => SettingPageState(); } @@ -40,10 +35,10 @@ class SettingPageState extends State { // 백엔드 모델과 동일한 변수명을 사용하기 위해 snake case 사용함. /// 성인글 보기 설정. true이면 성인글을 보여줌. - late bool see_sexual; + late bool seeSexual; /// 정치글 보기 설정. true이면 정치글을 보여줌. - late bool see_social; + late bool seeSocial; bool _isLoading = false; @@ -51,8 +46,8 @@ class SettingPageState extends State { void initState() { super.initState(); var userProvider = context.read(); - see_sexual = userProvider.naUser?.see_sexual ?? true; - see_social = userProvider.naUser?.see_social ?? true; + seeSexual = userProvider.naUser?.see_sexual ?? true; + seeSocial = userProvider.naUser?.see_social ?? true; // 페이지 전환 과정에서 새로운 알림을 확인하기 위한 호출. context.read().checkIsNotReadExist(userProvider); } @@ -68,13 +63,18 @@ class SettingPageState extends State { centerTitle: true, leading: IconButton( color: ColorsInfo.newara, - icon: SvgPicture.asset('assets/icons/left_chevron.svg', - color: ColorsInfo.newara, width: 35, height: 35), + icon: SvgPicture.asset( + 'assets/icons/left_chevron.svg', + colorFilter: + const ColorFilter.mode(ColorsInfo.newara, BlendMode.srcIn), + width: 35, + height: 35, + ), onPressed: () => Navigator.pop(context), ), title: SizedBox( child: Text( - 'setting_page.title'.tr(), + LocaleKeys.settingPage_title.tr(), style: const TextStyle( color: ColorsInfo.newara, fontSize: 18, @@ -104,7 +104,7 @@ class SettingPageState extends State { height: 34, ), Text( - 'setting_page.bulletin'.tr(), + LocaleKeys.settingPage_postSetting.tr(), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, @@ -137,7 +137,7 @@ class SettingPageState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - "setting_page.adult".tr(), + LocaleKeys.settingPage_adult.tr(), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -152,23 +152,27 @@ class SettingPageState extends State { fit: BoxFit.fill, child: CupertinoSwitch( activeColor: ColorsInfo.newara, - value: see_sexual, + value: seeSexual, onChanged: (value) async { - setState(() => see_sexual = value); - try { - await dio.patch( - '$newAraDefaultUrl/api/user_profiles/${userProvider.naUser!.user}/', - data: {'see_sexual': value}); + setState(() => seeSexual = value); + Response? patchRes = + await userProvider.patchApiRes( + 'user_profiles/${userProvider.naUser!.user}/', + data: {'see_sexual': value}); + if (patchRes != null) { await userProvider.apiMeUserInfo(); debugPrint( "Change of 'see_sexual' succeed!"); - requestSnackBar("설정이 저장되었습니다."); - } catch (error) { + requestSnackBar(LocaleKeys + .settingPage_settingsSaved + .tr()); + } else { debugPrint( - "Change of 'see_sexual' failed: $error"); - requestSnackBar( - "에러가 발생하여 설정 반영에 실패했습니다. 다시 시도해주십시오."); - setState(() => see_sexual = !value); + "Change of 'see_sexual' failed"); + requestSnackBar(LocaleKeys + .settingPage_errorSavingSettings + .tr()); + setState(() => seeSexual = !value); } }), ), @@ -184,7 +188,7 @@ class SettingPageState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - "setting_page.politics".tr(), + LocaleKeys.settingPage_politics.tr(), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -199,25 +203,29 @@ class SettingPageState extends State { fit: BoxFit.fill, child: CupertinoSwitch( activeColor: ColorsInfo.newara, - value: see_social, + value: seeSocial, onChanged: (value) async { setState(() { - see_social = value; + seeSocial = value; }); - try { - await dio.patch( - '$newAraDefaultUrl/api/user_profiles/${userProvider.naUser!.user}/', - data: {'see_social': value}); + Response? patchRes = + await userProvider.patchApiRes( + 'user_profiles/${userProvider.naUser!.user}/', + data: {'see_social': value}); + if (patchRes != null) { await userProvider.apiMeUserInfo(); debugPrint( "Change of 'see_social' succeed!"); - requestSnackBar("설정이 저장되었습니다."); - } catch (error) { + requestSnackBar(LocaleKeys + .settingPage_settingsSaved + .tr()); + } else { debugPrint( - "Change of 'see_social' failed: $error"); - requestSnackBar( - "에러가 발생하여 설정 반영에 실패했습니다. 다시 시도해주십시오."); - setState(() => see_social = !value); + "Change of 'see_social' failed"); + requestSnackBar(LocaleKeys + .settingPage_errorSavingSettings + .tr()); + setState(() => seeSocial = !value); } }), ), @@ -270,7 +278,7 @@ class SettingPageState extends State { height: 34, ), Text( - 'setting_page.block'.tr(), + LocaleKeys.settingPage_block.tr(), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, @@ -299,14 +307,15 @@ class SettingPageState extends State { const BlockedUserDialog()); }, child: Column( + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox(height: 13), SizedBox( width: MediaQuery.of(context).size.width - 60, child: Center( child: Text( - 'setting_page.blocked_users'.tr(), + LocaleKeys.settingPage_viewBlockedUsers + .tr(), style: const TextStyle( color: ColorsInfo.newara, fontSize: 16, @@ -321,8 +330,7 @@ class SettingPageState extends State { ), const SizedBox(height: 5), // 유저 차단 기능 설명 문구 - const TextInfo( - '유저 차단은 게시글의 더보기 기능에서 하실 수 있습니다.\n하루에 최대 10번만 변경 가능합니다.'), + TextInfo(LocaleKeys.settingPage_userBlockingGuide.tr()), const SizedBox(height: 20), SizedBox( width: MediaQuery.of(context).size.width - 50, @@ -333,9 +341,9 @@ class SettingPageState extends State { width: 36, height: 36, ), - const Text( - '정보', - style: TextStyle( + Text( + LocaleKeys.settingPage_information.tr(), + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, ), @@ -356,17 +364,21 @@ class SettingPageState extends State { ), ), child: Column( + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 10), // 이용약관 InkWell( - onTap: () async { - await Navigator.of(context).push( - slideRoute( - const TermsAndConditionsPage(), - ), - ); + onTap: () { + // TermsAndConditionsPage의 변경된 locale을 즉시 적용하기 위해 setState 호출함. + Navigator.of(context) + .push( + slideRoute( + const TermsAndConditionsPage(), + ), + ) + .then((_) => setState(() {})); }, child: Row( mainAxisAlignment: @@ -374,9 +386,11 @@ class SettingPageState extends State { children: [ Container( margin: const EdgeInsets.only(left: 10), - child: const Text( - '이용약관', - style: TextStyle( + child: Text( + LocaleKeys + .settingPage_termsAndConditions + .tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), @@ -400,9 +414,10 @@ class SettingPageState extends State { // 정치글 보기 글씨 Container( margin: const EdgeInsets.only(left: 10), - child: const Text( - '운영진에게 문의하기', - style: TextStyle( + child: Text( + LocaleKeys.settingPage_contactAdmins + .tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), @@ -446,10 +461,10 @@ class SettingPageState extends State { ), ); }, - child: const Center( + child: Center( child: Text( - '로그아웃', - style: TextStyle( + LocaleKeys.settingPage_signOut.tr(), + style: const TextStyle( color: ColorsInfo.newara, fontSize: 16, fontWeight: FontWeight.w500, @@ -482,16 +497,21 @@ class SettingPageState extends State { setState(() { _isLoading = true; }); - Map? responseResult = - await userProvider - .getApiRes('unregister'); + var response = await userProvider + .getApiRes('unregister'); + // ignore: unused_local_variable + final Map? responseResult = + await response?.data; + //TODO: 회원탈퇴 로직 보강 필요 // if(responseResult == null){ // ///회원탈퇴 실패 // } final prefs = await SharedPreferences.getInstance(); - String jsonString=userProvider.naUser!.user.toString(); // 데이터를 JSON 문자열로 인코딩 + String jsonString = userProvider + .naUser!.user + .toString(); // 데이터를 JSON 문자열로 인코딩 await prefs.setString( '심사통과를위한탈퇴탈퇴한유저', jsonString); @@ -507,10 +527,10 @@ class SettingPageState extends State { ), ); }, - child: const Center( + child: Center( child: Text( - '회원탈퇴', - style: TextStyle( + LocaleKeys.settingPage_withdrawal.tr(), + style: const TextStyle( color: ColorsInfo.newara, fontSize: 16, fontWeight: FontWeight.w500, @@ -521,8 +541,7 @@ class SettingPageState extends State { ), ), const SizedBox(height: 5), - const TextInfo( - '회원 탈퇴는 Ara 관리자가 확인 후 처리해드리며, 최대 24시간이 소요될 수 있습니다'), + TextInfo(LocaleKeys.settingPage_withdrawalGuide.tr()), ], ), ), @@ -585,7 +604,7 @@ class SettingPageState extends State { )) { debugPrint('Could not launch mail'); debugPrint("기본 메일앱을 열 수 없습니다."); - requestSnackBar("기본 메일 어플리케이션을 열 수 없습니다. ara@sparcs.org로 문의 부탁드립니다."); + requestSnackBar(LocaleKeys.settingPage_emailNotAvailable.tr()); return false; } diff --git a/lib/pages/sparcs_sso_page.dart b/lib/pages/sparcs_sso_page.dart index fcf7f339..19a08897 100644 --- a/lib/pages/sparcs_sso_page.dart +++ b/lib/pages/sparcs_sso_page.dart @@ -15,7 +15,7 @@ import 'package:provider/provider.dart'; /// SparcsSSOPage /// Sparcs SSO(단일 서명 인증) 처리를 담당하는 Flutter 페이지 위젯입니다. class SparcsSSOPage extends StatefulWidget { - const SparcsSSOPage({Key? key}) : super(key: key); + const SparcsSSOPage({super.key}); @override State createState() => _SparcsSSOPageState(); @@ -110,7 +110,7 @@ class _SparcsSSOPageState extends State { // 탈퇴했던 유저 아이디를 로컬에 저장하고 이를 로그인 할 때 확인하여 탈퇴한 유저임을 확인 if (!mounted) return; Navigator.of(context) - .pushReplacement(slideRoute(const InQuiryPage())); + .pushReplacement(slideRoute(const InquiryPage())); return; } @@ -126,7 +126,7 @@ class _SparcsSSOPageState extends State { // 회원 탈퇴일이 1900년 1월 1일 이후라면 탈퇴한 유저 if (!mounted) return; Navigator.of(context) - .pushReplacement(slideRoute(const InQuiryPage())); + .pushReplacement(slideRoute(const InquiryPage())); return; } } catch (e) { diff --git a/lib/pages/specific_bulletin_board_page.dart b/lib/pages/specific_bulletin_board_page.dart deleted file mode 100644 index 82ff6f95..00000000 --- a/lib/pages/specific_bulletin_board_page.dart +++ /dev/null @@ -1,207 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:new_ara_app/constants/colors_info.dart'; -import 'package:new_ara_app/providers/user_provider.dart'; -import 'package:new_ara_app/widgets/loading_indicator.dart'; -import 'package:new_ara_app/widgets/post_preview.dart'; -import 'package:provider/provider.dart'; - -import 'package:new_ara_app/models/article_list_action_model.dart'; - -// TODO: free_bulletin_board_page.dart와 통합 필요. -class SpecificBulletinBoardPage extends StatefulWidget { - const SpecificBulletinBoardPage({Key? key}) : super(key: key); - - @override - State createState() => - _SpecificBulletinBoardPageState(); -} - -class _SpecificBulletinBoardPageState extends State { - List postList = []; - int currentPageNumber = 1; - bool isPageLoading = true; - final ScrollController postsScrollController = ScrollController(); - - @override - void initState() { - super.initState(); - - var userProvider = context.read(); - postsScrollController.addListener(loadMorePostsListener); - refreshPosts(userProvider); - } - - @override - void dispose() { - postsScrollController.removeListener(loadMorePostsListener); - postsScrollController.dispose(); - super.dispose(); - } - - void refreshPosts(UserProvider userProvider) async { - Map? apiResponseMap = - await userProvider.getApiRes("articles/?parent_board=7&page=1"); - if (mounted) { - setState(() { - postList.clear(); - for (int i = 0; i < (apiResponseMap?["results"].length ?? 0); i++) { - postList.add(ArticleListActionModel.fromJson( - apiResponseMap?["results"][i] ?? {})); - } - isPageLoading = false; - }); - } - } - - void loadMorePostsListener() async { - var userProvider = context.read(); - if (postsScrollController.position.pixels == - postsScrollController.position.maxScrollExtent) { - currentPageNumber = currentPageNumber + 1; - Map? apiResponseMap = await userProvider - .getApiRes("articles/?parent_board=7&page=$currentPageNumber"); - if (mounted) { - setState(() { - for (int i = 0; i < (apiResponseMap?["results"].length ?? 0); i++) { - if (apiResponseMap?["results"][i]["created_by"]["profile"] != - null && - apiResponseMap?["results"][i]["is_hidden"] == false) { - postList.add(ArticleListActionModel.fromJson( - apiResponseMap?["results"][i] ?? {})); - } - } - isPageLoading = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - centerTitle: true, - leadingWidth: 100, - leading: Row( - children: [ - SizedBox( - width: 35, - child: IconButton( - color: ColorsInfo.newara, - icon: SizedBox( - width: 11.58, - height: 21.87, - child: SvgPicture.asset( - 'assets/icons/left_chevron.svg', - color: ColorsInfo.newara, - fit: BoxFit.fill, - ), - ), - onPressed: () { - Navigator.pop(context); - }, - ), - ), - const Text( - "게시판", - style: TextStyle( - color: Color(0xFFED3A3A), - fontSize: 17, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - actions: [ - IconButton( - icon: SvgPicture.asset( - 'assets/icons/search.svg', - color: ColorsInfo.newara, - width: 35, - height: 35, - ), - onPressed: () {}, - ), - ], - ), - floatingActionButton: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FloatingActionButton( - onPressed: () { - debugPrint('FloatingActionButton pressed'); - }, - backgroundColor: Colors.white, - child: SizedBox( - width: 42, - height: 42, - child: SvgPicture.asset( - 'assets/icons/modify.svg', - fit: BoxFit.fill, - ), - ), - ), - const SizedBox( - height: 20, - ) - ], - ), - body: isPageLoading - ? const LoadingIndicator() - : SafeArea( - child: Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 18, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const SizedBox( - width: 11, - ), - const Expanded( - child: Text( - "학교에게 전합니다.", - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w800, - color: ColorsInfo.newara, - ), - ), - ), - SvgPicture.asset('assets/icons/information.svg') - ], - ), - const SizedBox( - height: 106, - ), - Expanded( - child: ListView.builder( - controller: postsScrollController, - itemCount: postList.length, - itemBuilder: (BuildContext context, int index) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(11.0), - child: PostPreview(model: postList[index]), - ), - Container( - height: 1, - color: const Color(0xFFF0F0F0), - ), - ], - ); - }, - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/terms_and_conditions_page.dart b/lib/pages/terms_and_conditions_page.dart index dca5f3ba..05d42653 100644 --- a/lib/pages/terms_and_conditions_page.dart +++ b/lib/pages/terms_and_conditions_page.dart @@ -4,12 +4,12 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:new_ara_app/constants/colors_info.dart'; -import 'package:new_ara_app/constants/url_info.dart'; import 'package:new_ara_app/providers/user_provider.dart'; -import 'package:new_ara_app/utils/create_dio_with_config.dart'; import 'package:new_ara_app/widgets/loading_indicator.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; class TermsAndConditionsPage extends StatefulWidget { const TermsAndConditionsPage({super.key}); @@ -31,7 +31,8 @@ class _TermsAndConditionsPageState extends State { centerTitle: true, title: SizedBox( child: Text( - "이용약관", + LocaleKeys.termsAndConditionsPage_termsAndConditions.tr(), + textAlign: TextAlign.center, style: const TextStyle( color: ColorsInfo.newara, fontSize: 18, @@ -39,24 +40,31 @@ class _TermsAndConditionsPageState extends State { ), ), ), - leadingWidth: 100, - leading: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => Navigator.pop(context), - child: Stack( - alignment: Alignment.centerLeft, - children: [ - SvgPicture.asset( - 'assets/icons/left_chevron.svg', - colorFilter: const ColorFilter.mode( - ColorsInfo.newara, BlendMode.srcATop), - fit: BoxFit.fill, - width: 35, - height: 35, - ), - ], + leading: IconButton( + color: ColorsInfo.newara, + icon: SvgPicture.asset( + 'assets/icons/left_chevron.svg', + colorFilter: + const ColorFilter.mode(ColorsInfo.newara, BlendMode.srcIn), + width: 35, + height: 35, ), + onPressed: () => Navigator.pop(context), ), + actions: [ + IconButton( + onPressed: () async { + if (context.locale == const Locale('ko')) { + await context.setLocale(const Locale('en')); + } else { + await context.setLocale(const Locale('ko')); + } + }, + icon: const Icon( + Icons.language, + color: ColorsInfo.newara, + )), + ], ), body: SafeArea( child: _isLoading @@ -74,197 +82,15 @@ class _TermsAndConditionsPageState extends State { child: Container( color: const Color(0xFFF6F6F6), child: Padding( - padding: const EdgeInsets.all(15.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildNormalText( - "new Ara (이하 아라) 이용약관은 현재 적용 중입니다.\n"), - _buildBoldText("제 1조. 아라의 목적"), - Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - _buildNormalText( - "1. 아라는 KAIST 구성원의 원활한 정보공유를 위해 KAIST 학부 동아리 SPARCS (이하 \"SPARCS\")에서 제공하는 공용 게시판 서비스 (Bulletin Board System) 입니다."), - _buildNormalText( - "2. 1조 1항에서의 KAIST 구성원이란 교수, 교직원, 그리고 재학생과 졸업생, 입주 업체 등을 나타냅니다.\n"), - ], - ), - ), - _buildBoldText("제 2조. 가입 및 탈퇴"), - Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - _buildNormalText( - "1. 아라는 KAIST 구성원만 이용 가능합니다."), - _buildNormalText( - "2. 아라는 SPARCS SSO를 통해 가입할 수 있습니다."), - _buildNormalText( - " - SPARCS SSO에서 카이스트 통합인증으로 가입시 별도 승인 없이 바로 서비스 이용이 가능합니다. (교수, 교직원, 재학생, 졸업생 등)"), - _buildNormalText( - " - SPARCS SSO에서 카이스트 통합인증 외 다른 방법으로 가입시 아라 운영진이 승인해야만 서비스 이용이 가능합니다. (입주 업체 등)"), - _buildNormalText( - "3. 아라는 회원탈퇴 기능이 없습니다. 다만, 아라 운영진에게 회원 탈퇴를 요청할 수 있습니다."), - _buildNormalText( - "4. 다음의 경우에는 회원자격이 박탈될 수 있습니다."), - _buildNormalText( - " - 카이스트 구성원이 아닌 것으로 밝혀졌을 경우"), - _buildNormalText( - " - new Ara 이용약관에 명시된 회원의 의무를 지키지 않은 경우"), - _buildNormalText( - " - 아라 이용 중 정보통신망 이용촉진 및 정보보호 등에 관한 법률 및 관계 법령과 본 약관이 금지하거나 공서양속에 반하는 행위를 하는 경우\n"), - ], - ), - ), - _buildBoldText("제 3조. 회원의 의무"), - Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - _buildNormalText( - "1. 회원은 아라 이용과 관련하여 다음의 행위를 하여서는 안 됩니다."), - _buildNormalText( - " - SPARCS, 아라 운영진, 또는 특정 개인 및 단체를 사칭하는 행위"), - _buildNormalText( - " - 아라를 이용하여 얻은 정보를 원작자나 아라 운영진의 사전 승낙 없이 복사, 복제, 변경, 번역, 출판, 방송, 기타의 방법으로 사용하거나 이를 타인에게 제공하는 행위"), - _buildNormalText( - " - 다른 회원의 계정을 부정 사용하는 행위"), - _buildNormalText( - " - 타인의 명예를 훼손하거나 모욕하는 행위"), - _buildNormalText( - " - 타인의 지적재산권 등의 권리를 침해하는 행위"), - _buildNormalText( - " - 해킹행위 또는 컴퓨터바이러스의 유포 행위"), - _buildNormalText( - " - 광고성 정보 등 일정한 내용을 지속적으로 전송하는 행위"), - _buildNormalText( - " - 서비스의 안전적인 운영에 지장을 주거나 줄 우려가 있는 일체의 행위"), - _buildNormalText( - " - 범죄행위를 목적으로 하거나 기타 범죄행위와 관련된 행위"), - _buildNormalText( - " - SPARCS의 동의 없이 아라를 영리목적으로 사용하는 행위"), - _buildNormalText( - " - 기타 아라의 커뮤니티 강령에 반하거나 아라 서비스 운영상 부적절하다고 판단하는 행위\n"), - ], - ), - ), - - _buildBoldText("제 4조. 게시물에 대한 권리"), - Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - _buildNormalText( - "1. 회원이 아라 내에 올린 게시물의 저작권은 게시한 회원에게 귀속됩니다."), - _buildNormalText( - "2. 서비스의 게시물 또는 내용물이 위의 약관에 위배될 경우 사전 통지나 동의 없이 삭제될 수 있습니다."), - _buildNormalText( - "3. 제 3조 회원의 의무에 따라, 아라를 이용하여 얻은 정보를 원작자나 아라 운영진의 사전 승낙 없이 복사, 복제, 변경, 번역, 출판, 방송, 기타의 방법으로 사용하거나, 영리목적으로 활용하거나, 이를 타인에게 제공하는 행위는 금지됩니다.\n"), - ], - ), - ), - - _buildBoldText("제 5조. 책임의 제한"), - Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - _buildNormalText( - "1. SPARCS는 다음의 사유로 서비스 제공을 중지하는 것에 대해 책임을 지지 않습니다."), - _buildNormalText( - " - 설비의 보수 등을 위해 부득이한 경우"), - _buildNormalText( - " - KAIST가 전기통신서비스를 중지하는 경우"), - _buildNormalText( - " - 천재지변, 정전 및 전시 상황인 경우"), - _buildNormalText( - " - 기타 본 서비스를 제공할 수 없는 사유가 발생한 경우"), - _buildNormalText( - "2. SPARCS는 다음의 사항에 대해 책임을 지지 않습니다."), - _buildNormalText( - " - 개재된 회원들의 글에 대한 신뢰도, 정확도"), - _buildNormalText( - " - 아라를 매개로 회원 상호 간 및 회원과 제 3자 간에 발생한 분쟁"), - _buildNormalText( - " - 기타 아라 사용 중 발생한 피해 및 분쟁\n"), - ], - ), - ), - - _buildBoldText("제 6조. 문의 및 제보"), - Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - _buildNormalText( - "1. 아라에 대한 건의사항 또는 버그에 대한 사항은 구글폼을 통해 문의 및 제보할 수 있습니다."), - Text.rich( - TextSpan( - text: - 'https://sparcs.page.link/newara-feedback', - style: const TextStyle( - color: Colors.blue), - recognizer: TapGestureRecognizer() - ..onTap = () async { - await launchUrl(Uri.parse( - 'https://sparcs.page.link/newara-feedback')); - }, - ), - ), - _buildNormalText( - "2. 6조 1항의 구글폼이 작동하지 않거나, 기타 사항의 경우 new-ara@sparcs.org 를 통해 문의 및 제보할 수 있습니다.\n"), - ], - ), - ), - _buildBoldText("제 7조. 게시, 개정 및 해석"), - Padding( - padding: const EdgeInsets.only(left: 20.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - _buildNormalText( - "1. 아라 운영진은 본 약관에 대해 아라 회원가입시 회원의 동의를 받습니다."), - _buildNormalText( - "2. 아라 운영진은 약관의규제에관한법률, 정보통신망이용촉진및정보보호등에관한법률 등 관련법을 위배하지 않는 범위에서 본 약관을 개정할 수 있습니다."), - _buildNormalText( - "3. 본 약관을 개정하는 경우 적용일자, 개정 내용 및 사유를 명시하여 개정 약관의 적용일자 7일 전부터 적용일자 전일까지 아라의 '뉴아라 공지' 게시판을 통해 공지합니다."), - _buildNormalText( - "4. 회원은 개정약관이 공지된 지 7일 내에 개정약관에 대한 거부의 의사표시를 할 수 있습니다. 이 경우 회원은 아라 운영진에게 메일을 발송하여 즉시 사용 중인 모든 지원 서비스를 해지하고 본 서비스에서 회원 탈퇴할 수 있습니다."), - _buildNormalText( - "5. 아라 운영진은 개정약관이 공지된 지 7일 내에 거부의 의사표시를 하지 않은 회원에 대해 개정약관에 대해 동의한 것으로 간주합니다."), - _buildNormalText( - "6. 본 약관의 해석은 아라 운영진이 담당하며, 분쟁이 있을 경우 민법 등 관계 법률과 관례에 따릅니다.\n"), - ], - ), - ), - - _buildNormalText("본 약관은 2020-09-26부터 적용됩니다."), - - // 제 3조 이후의 내용도 같은 방식으로 추가... - ], - ), - ), + padding: const EdgeInsets.all(15.0), + child: _buildTermsAndConditionsText( + context.locale)), ), ), ), ), ), - SizedBox( + const SizedBox( height: 0, ), Row( @@ -287,7 +113,7 @@ class _TermsAndConditionsPageState extends State { }); Response? res = await userProvider.patchApiRes( 'user_profiles/${userProvider.naUser!.user}/agree_terms_of_service/', - payload: {}); + data: {}); if (res != null && (res.statusCode == 200 || res.statusCode == 400)) { @@ -309,10 +135,11 @@ class _TermsAndConditionsPageState extends State { borderRadius: BorderRadius.circular(10.0), color: ColorsInfo.newara, ), - child: const Center( + child: Center( child: Text( - "동의 하기", - style: TextStyle( + LocaleKeys.termsAndConditionsPage_agree + .tr(), + style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, @@ -322,8 +149,8 @@ class _TermsAndConditionsPageState extends State { ), ) : Text( - "이미 동의하셨습니다.", - style: TextStyle( + LocaleKeys.termsAndConditionsPage_agreed.tr(), + style: const TextStyle( color: ColorsInfo.newara, fontSize: 16, fontWeight: FontWeight.w500, @@ -333,7 +160,7 @@ class _TermsAndConditionsPageState extends State { ) ], ), - SizedBox( + const SizedBox( height: 30, ), ], @@ -348,7 +175,6 @@ class _TermsAndConditionsPageState extends State { style: const TextStyle( color: Color(0xFF4a4a4a), fontWeight: FontWeight.w500, - fontFamily: 'NotoSansKR', height: 1.6, fontSize: 16, ), @@ -360,11 +186,340 @@ class _TermsAndConditionsPageState extends State { text, style: const TextStyle( color: Color(0xff363636), - fontFamily: 'NotoSansKR', fontWeight: FontWeight.w700, height: 1.6, fontSize: 16, ), ); } + + Widget _buildTermsAndConditionsText(Locale locale) { + return locale == const Locale('en') + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + 'The terms and conditions of new Ara ("Ara") are currently in effect.\n'), + _buildBoldText("I. Ara's Purpose"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + '1. Ara is a public bulletin board system provided by KAIST undergraduate club SPARCS ("SPARCS") for the smooth sharing of information among KAIST members.'), + _buildNormalText( + "2. In section I.1, KAIST members refer to professors, faculty, students, graduates, tenant companies, etc.\n"), + ], + ), + ), + _buildBoldText("II. Register and Withdraw"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. Ara is only available to KAIST members."), + _buildNormalText( + "2. Ara can be registered through SPARCS SSO."), + _buildNormalText( + " - When you sign up with KAIST IAM in SPARCS SSO, you can use the service immediately without any additional approval (professors, faculty, students, graduates, etc.)."), + _buildNormalText( + " - When you sign up with other methods without KAIST IAM in SPARCS SSO, The service can only be used by the Ara management team (such as tenant companies)."), + _buildNormalText( + "3. Ara does not have a withdrawal function. However, you can request Ara's management team to withdraw membership."), + _buildNormalText( + "4. Membership may be revoked in the following cases:"), + _buildNormalText( + " - If found not to be a member of KAIST;"), + _buildNormalText( + " - Failure to comply with the obligations of the members specified in the Ara's Terms of Service;"), + _buildNormalText( + " - Where the Act on Promotion of Information and Communications Network Utilization and Information Protection, the related statute, prohibited acts by the Terms of Service or an act contrary to the public order during the use of Ara;\n"), + ], + ), + ), + _buildBoldText("III. Member's duty"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. The member shall not perform any of the following acts in connection with the use of Ara:."), + _buildNormalText( + " - Impersonating SPARCS, Ara management team, or a specific individual or organization."), + _buildNormalText( + " - Copying, duplicating, changing, translating, publishing, broadcasting, or providing information obtained using Ara to others without prior consent from the authorship or Ara management team."), + _buildNormalText( + " - Fraudulently using another member's account"), + _buildNormalText(" - Defaming or insulting others."), + _buildNormalText( + " - Infringing on the rights of others, such as intellectual property, etc."), + _buildNormalText( + " - Hacking or distributing computer viruses"), + _buildNormalText( + " - Continuously transmitting certain contents, such as advertising information."), + _buildNormalText( + " - Any act that is likely to interfere with or hinder the safe operation of the service."), + _buildNormalText( + " - Any act aimed at or related to a criminal act."), + _buildNormalText( + " - The act of using Ara for profit without the consent of SPARCS."), + _buildNormalText( + " - Other acts against Ara's Community Code or deemed inappropriate in the operation of Ara services.\n"), + ], + ), + ), + + _buildBoldText("IV. Right to Post"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. The copyright of the posting posted within Ara belongs to the member who posted it."), + _buildNormalText( + "2. If the posts or contents of the service violate the terms of service above, they may be deleted without prior notice or consent."), + _buildNormalText( + "3. In accordance with the obligations of the members of III, copying, changing, translating, publishing, broadcasting, or providing information obtained using Ara to others is prohibited without prior consent from the original author or Ara management team.\n"), + ], + ), + ), + + _buildBoldText("V. Limitation of Liability"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. SPARCS is not responsible for discontinuing the service due to the following reasons:"), + _buildNormalText( + " - In case it is inevitable for repair of equipment, etc."), + _buildNormalText( + " - When KAIST stops telecommunication service"), + _buildNormalText( + " - In case of natural disasters, power outages and wartime situations"), + _buildNormalText( + " - Other reasons for not being able to provide this service arise."), + _buildNormalText( + "2. SPARCS is not responsible for the following matters:."), + _buildNormalText( + " - Accuracy, and reliability of the members' posts."), + _buildNormalText( + " - Disputes arising between members and and third parties through Ara's medium"), + _buildNormalText( + " - Other damages and disputes arising from the use of Ara\n"), + ], + ), + ), + + _buildBoldText("VI. Inquiries and reports"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. Suggestions for Ara or bug can be inquired and reported through Google forms."), + Text.rich( + TextSpan( + text: 'https://sparcs.page.link/newara-feedback', + style: const TextStyle(color: Colors.blue), + recognizer: TapGestureRecognizer() + ..onTap = () async { + await launchUrl(Uri.parse( + 'https://sparcs.page.link/newara-feedback')); + }, + ), + ), + _buildNormalText( + "2. If the Google Form does not work or if there is anything else to inquire, you can contact and inform us at new-ara@sparcs.org.\n"), + ], + ), + ), + _buildBoldText("VII. Publish, revise, and interpret"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. The Ara management team obtains the consent of the members when signing up for Ara membership in this Agreement."), + _buildNormalText( + "2. The Ara management team may amend this Agreement to the extent that it does not violate the relevant laws, such as the Act on the Regulation of Terms and Conditions and the Act on the Promotion of Information and Communication Network Utilization and Information Protection, etc."), + _buildNormalText( + "3. In the event of amending this Agreement, the date of application, details and reasons for the amendment shall be stated and notified through the 'New Ara Notice' bulletin board from 7 days before the date of application to the day before the date of application."), + _buildNormalText( + "4. The member may express his/her rejection of the revised terms within 7 days after the announcement of the revised terms of service. In this case, the member may immediately terminate all support services in use by sending an email to Ara management team and withdraw from this service."), + _buildNormalText( + "5. The Ara management team considers the members who have not expressed their rejection within 7 days of the announcement of the revised terms to have agreed to the amended terms of service."), + _buildNormalText( + "6. The interpretation of these terms and conditions is handled by the Ara management team, and if there is a dispute, it is in accordance with relevant laws and practices, such as civil law.\n"), + ], + ), + ), + + _buildNormalText("These terms of service apply from 2020-09-26."), + + // 제 3조 이후의 내용도 같은 방식으로 추가... + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText("new Ara (이하 아라) 이용약관은 현재 적용 중입니다.\n"), + _buildBoldText("제 1조. 아라의 목적"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. 아라는 KAIST 구성원의 원활한 정보공유를 위해 KAIST 학부 동아리 SPARCS (이하 \"SPARCS\")에서 제공하는 공용 게시판 서비스 (Bulletin Board System) 입니다."), + _buildNormalText( + "2. 1조 1항에서의 KAIST 구성원이란 교수, 교직원, 그리고 재학생과 졸업생, 입주 업체 등을 나타냅니다.\n"), + ], + ), + ), + _buildBoldText("제 2조. 가입 및 탈퇴"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText("1. 아라는 KAIST 구성원만 이용 가능합니다."), + _buildNormalText("2. 아라는 SPARCS SSO를 통해 가입할 수 있습니다."), + _buildNormalText( + " - SPARCS SSO에서 카이스트 통합인증으로 가입시 별도 승인 없이 바로 서비스 이용이 가능합니다. (교수, 교직원, 재학생, 졸업생 등)"), + _buildNormalText( + " - SPARCS SSO에서 카이스트 통합인증 외 다른 방법으로 가입시 아라 운영진이 승인해야만 서비스 이용이 가능합니다. (입주 업체 등)"), + _buildNormalText( + "3. 아라는 회원탈퇴 기능이 없습니다. 다만, 아라 운영진에게 회원 탈퇴를 요청할 수 있습니다."), + _buildNormalText("4. 다음의 경우에는 회원자격이 박탈될 수 있습니다."), + _buildNormalText(" - 카이스트 구성원이 아닌 것으로 밝혀졌을 경우"), + _buildNormalText(" - new Ara 이용약관에 명시된 회원의 의무를 지키지 않은 경우"), + _buildNormalText( + " - 아라 이용 중 정보통신망 이용촉진 및 정보보호 등에 관한 법률 및 관계 법령과 본 약관이 금지하거나 공서양속에 반하는 행위를 하는 경우\n"), + ], + ), + ), + _buildBoldText("제 3조. 회원의 의무"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText("1. 회원은 아라 이용과 관련하여 다음의 행위를 하여서는 안 됩니다."), + _buildNormalText( + " - SPARCS, 아라 운영진, 또는 특정 개인 및 단체를 사칭하는 행위"), + _buildNormalText( + " - 아라를 이용하여 얻은 정보를 원작자나 아라 운영진의 사전 승낙 없이 복사, 복제, 변경, 번역, 출판, 방송, 기타의 방법으로 사용하거나 이를 타인에게 제공하는 행위"), + _buildNormalText(" - 다른 회원의 계정을 부정 사용하는 행위"), + _buildNormalText(" - 타인의 명예를 훼손하거나 모욕하는 행위"), + _buildNormalText(" - 타인의 지적재산권 등의 권리를 침해하는 행위"), + _buildNormalText(" - 해킹행위 또는 컴퓨터바이러스의 유포 행위"), + _buildNormalText(" - 광고성 정보 등 일정한 내용을 지속적으로 전송하는 행위"), + _buildNormalText( + " - 서비스의 안전적인 운영에 지장을 주거나 줄 우려가 있는 일체의 행위"), + _buildNormalText(" - 범죄행위를 목적으로 하거나 기타 범죄행위와 관련된 행위"), + _buildNormalText(" - SPARCS의 동의 없이 아라를 영리목적으로 사용하는 행위"), + _buildNormalText( + " - 기타 아라의 커뮤니티 강령에 반하거나 아라 서비스 운영상 부적절하다고 판단하는 행위\n"), + ], + ), + ), + + _buildBoldText("제 4조. 게시물에 대한 권리"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. 회원이 아라 내에 올린 게시물의 저작권은 게시한 회원에게 귀속됩니다."), + _buildNormalText( + "2. 서비스의 게시물 또는 내용물이 위의 약관에 위배될 경우 사전 통지나 동의 없이 삭제될 수 있습니다."), + _buildNormalText( + "3. 제 3조 회원의 의무에 따라, 아라를 이용하여 얻은 정보를 원작자나 아라 운영진의 사전 승낙 없이 복사, 복제, 변경, 번역, 출판, 방송, 기타의 방법으로 사용하거나, 영리목적으로 활용하거나, 이를 타인에게 제공하는 행위는 금지됩니다.\n"), + ], + ), + ), + + _buildBoldText("제 5조. 책임의 제한"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. SPARCS는 다음의 사유로 서비스 제공을 중지하는 것에 대해 책임을 지지 않습니다."), + _buildNormalText(" - 설비의 보수 등을 위해 부득이한 경우"), + _buildNormalText(" - KAIST가 전기통신서비스를 중지하는 경우"), + _buildNormalText(" - 천재지변, 정전 및 전시 상황인 경우"), + _buildNormalText(" - 기타 본 서비스를 제공할 수 없는 사유가 발생한 경우"), + _buildNormalText("2. SPARCS는 다음의 사항에 대해 책임을 지지 않습니다."), + _buildNormalText(" - 개재된 회원들의 글에 대한 신뢰도, 정확도"), + _buildNormalText( + " - 아라를 매개로 회원 상호 간 및 회원과 제 3자 간에 발생한 분쟁"), + _buildNormalText(" - 기타 아라 사용 중 발생한 피해 및 분쟁\n"), + ], + ), + ), + + _buildBoldText("제 6조. 문의 및 제보"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. 아라에 대한 건의사항 또는 버그에 대한 사항은 구글폼을 통해 문의 및 제보할 수 있습니다."), + Text.rich( + TextSpan( + text: 'https://sparcs.page.link/newara-feedback', + style: const TextStyle(color: Colors.blue), + recognizer: TapGestureRecognizer() + ..onTap = () async { + await launchUrl(Uri.parse( + 'https://sparcs.page.link/newara-feedback')); + }, + ), + ), + _buildNormalText( + "2. 6조 1항의 구글폼이 작동하지 않거나, 기타 사항의 경우 new-ara@sparcs.org 를 통해 문의 및 제보할 수 있습니다.\n"), + ], + ), + ), + _buildBoldText("제 7조. 게시, 개정 및 해석"), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildNormalText( + "1. 아라 운영진은 본 약관에 대해 아라 회원가입시 회원의 동의를 받습니다."), + _buildNormalText( + "2. 아라 운영진은 약관의규제에관한법률, 정보통신망이용촉진및정보보호등에관한법률 등 관련법을 위배하지 않는 범위에서 본 약관을 개정할 수 있습니다."), + _buildNormalText( + "3. 본 약관을 개정하는 경우 적용일자, 개정 내용 및 사유를 명시하여 개정 약관의 적용일자 7일 전부터 적용일자 전일까지 아라의 '뉴아라 공지' 게시판을 통해 공지합니다."), + _buildNormalText( + "4. 회원은 개정약관이 공지된 지 7일 내에 개정약관에 대한 거부의 의사표시를 할 수 있습니다. 이 경우 회원은 아라 운영진에게 메일을 발송하여 즉시 사용 중인 모든 지원 서비스를 해지하고 본 서비스에서 회원 탈퇴할 수 있습니다."), + _buildNormalText( + "5. 아라 운영진은 개정약관이 공지된 지 7일 내에 거부의 의사표시를 하지 않은 회원에 대해 개정약관에 대해 동의한 것으로 간주합니다."), + _buildNormalText( + "6. 본 약관의 해석은 아라 운영진이 담당하며, 분쟁이 있을 경우 민법 등 관계 법률과 관례에 따릅니다.\n"), + ], + ), + ), + + _buildNormalText("본 약관은 2020-09-26부터 적용됩니다."), + + // 제 3조 이후의 내용도 같은 방식으로 추가... + ], + ); + } } diff --git a/lib/pages/user_page.dart b/lib/pages/user_page.dart index 66590ec4..ef903d09 100644 --- a/lib/pages/user_page.dart +++ b/lib/pages/user_page.dart @@ -1,8 +1,9 @@ -/// 사용자 본인 정보 표시 페이지를 관리하는 파일 +// 사용자 본인 정보 표시 페이지를 관리하는 파일 import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:new_ara_app/constants/url_info.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:provider/provider.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -13,12 +14,10 @@ import 'package:new_ara_app/widgets/loading_indicator.dart'; import 'package:new_ara_app/models/article_list_action_model.dart'; import 'package:new_ara_app/models/scrap_model.dart'; import 'package:new_ara_app/pages/post_view_page.dart'; -import 'package:new_ara_app/utils/time_utils.dart'; import 'package:new_ara_app/utils/slide_routing.dart'; import 'package:new_ara_app/pages/profile_edit_page.dart'; import 'package:new_ara_app/providers/notification_provider.dart'; import 'package:new_ara_app/utils/profile_image.dart'; -import 'package:new_ara_app/utils/handle_hidden.dart'; import 'package:new_ara_app/widgets/post_preview.dart'; /// 작성한 글, 담아둔 글, 최근 본 글을 나타내기 위해 사용 @@ -26,7 +25,7 @@ enum TabType { created, scrap, recent } /// 사용자 본인 정보 페이지의 빌드 및 이벤트 처리를 담당하는 위젯 class UserPage extends StatefulWidget { - const UserPage({Key? key}) : super(key: key); + const UserPage({super.key}); @override State createState() => _UserPageState(); } @@ -70,17 +69,17 @@ class _UserPageState extends State /// 현재 로드한 마지막 페이지를 나타냄. List curPage = [1, 1, 1]; - final List _tabs = [ - 'myPage.mypost'.tr(), - 'myPage.scrap'.tr(), - 'myPage.recent'.tr() + List tabs = [ + LocaleKeys.userPage_myPosts.tr(), + LocaleKeys.userPage_bookmarks.tr(), + LocaleKeys.userPage_history.tr() ]; @override void initState() { super.initState(); _tabController = TabController( - length: _tabs.length, + length: tabs.length, vsync: this, ); @@ -188,12 +187,22 @@ class _UserPageState extends State splashColor: Colors.white, icon: SvgPicture.asset( 'assets/icons/setting.svg', - color: ColorsInfo.newara, + colorFilter: + const ColorFilter.mode(ColorsInfo.newara, BlendMode.srcIn), width: 35, height: 35, ), onPressed: () { - Navigator.of(context).push(slideRoute(const SettingPage())); + // SettingPage에서 변경된 locale을 즉시 반영하기 위해 setState를 호출함. + Navigator.of(context) + .push(slideRoute(const SettingPage())) + .then((_) => setState(() { + tabs = [ + LocaleKeys.userPage_myPosts.tr(), + LocaleKeys.userPage_bookmarks.tr(), + LocaleKeys.userPage_history.tr() + ]; + })); }, ), const SizedBox(width: 11), @@ -213,7 +222,7 @@ class _UserPageState extends State unselectedLabelColor: const Color.fromRGBO(177, 177, 177, 1), labelColor: ColorsInfo.newara, indicatorColor: ColorsInfo.newara, - tabs: _tabs.map((String tab) { + tabs: tabs.map((String tab) { return Tab(text: tab); }).toList(), controller: _tabController, @@ -227,7 +236,8 @@ class _UserPageState extends State width: MediaQuery.of(context).size.width - 40, height: 24, child: Text( - '총 $curCount개의 글', + LocaleKeys.userPage_totalNPosts + .tr(namedArgs: {'curCount': curCount.toString()}), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, @@ -306,7 +316,7 @@ class _UserPageState extends State Text( userProvider.naUser == null || userProvider.naUser?.email == null - ? "이메일 정보가 없습니다." + ? LocaleKeys.userPage_noEmailInfo.tr() : "${userProvider.naUser?.email}", style: const TextStyle( fontSize: 14, @@ -320,7 +330,7 @@ class _UserPageState extends State ), const SizedBox(width: 30), SizedBox( - width: 26, + width: 30, height: 21, child: GestureDetector( behavior: HitTestBehavior.translucent, @@ -332,7 +342,7 @@ class _UserPageState extends State ); }, child: Text( - 'myPage.change'.tr(), + LocaleKeys.userPage_change.tr(), style: const TextStyle( color: Color.fromRGBO(100, 100, 100, 1), fontSize: 14, @@ -467,11 +477,11 @@ class _UserPageState extends State String getApiUrl(TabType tabType, int page, int user) { switch (tabType) { case TabType.created: - return "/api/articles/?page=$page&created_by=$user"; + return "articles/?page=$page&created_by=$user"; case TabType.scrap: - return "/api/scraps/?page=$page&created_by=$user"; + return "scraps/?page=$page&created_by=$user"; case TabType.recent: - return "/api/articles/recent/?page=$page"; + return "articles/recent/?page=$page"; } } @@ -530,11 +540,10 @@ class _UserPageState extends State if (page == 1) clearList(tabType); try { - var response = await userProvider - .createDioWithHeadersForGet() - .get('$newAraDefaultUrl$apiUrl'); + var response = await userProvider.getApiRes(apiUrl); + final Map? jsonList = await response?.data; - List rawPostList = response.data['results']; + List rawPostList = jsonList?['results']; for (int i = 0; i < rawPostList.length; i++) { Map? rawPost = rawPostList[i]; if (rawPost != null) { @@ -547,7 +556,7 @@ class _UserPageState extends State } } // pageT에 해당하는 페이지의 글 개수를 업데이트함. - articleCount[tabType.index] = response.data['num_items']; + articleCount[tabType.index] = jsonList?['num_items']; debugPrint("fetchArticles() succeeded for page: $page"); return true; } on DioException catch (e) { diff --git a/lib/pages/user_view_page.dart b/lib/pages/user_view_page.dart index 33600b7f..612194c9 100644 --- a/lib/pages/user_view_page.dart +++ b/lib/pages/user_view_page.dart @@ -1,10 +1,11 @@ -/// 임의의 유저에 대한 닉네임, 프로필, 작성한 글 등을 보여주는 페이지 관리 파일. -/// article, comment에서 작성자의 정보를 확인하는 기능을 위해 만들어짐. +// 임의의 유저에 대한 닉네임, 프로필, 작성한 글 등을 보여주는 페이지 관리 파일. +// article, comment에서 작성자의 정보를 확인하는 기능을 위해 만들어짐. import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:new_ara_app/constants/colors_info.dart'; import 'package:new_ara_app/models/public_user_profile_model.dart'; @@ -12,15 +13,12 @@ import 'package:new_ara_app/models/article_list_action_model.dart'; import 'package:new_ara_app/providers/user_provider.dart'; import 'package:new_ara_app/widgets/loading_indicator.dart'; import 'package:new_ara_app/constants/url_info.dart'; -import 'package:new_ara_app/utils/time_utils.dart'; import 'package:new_ara_app/pages/post_view_page.dart'; import 'package:new_ara_app/utils/slide_routing.dart'; import 'package:new_ara_app/providers/notification_provider.dart'; import 'package:new_ara_app/utils/profile_image.dart'; -import 'package:new_ara_app/utils/handle_hidden.dart'; import 'package:new_ara_app/widgets/post_preview.dart'; - /// 유저 관련 정보 페이지 뷰, 이벤트 처리를 모두 관리하는 StatefulWidget /// **중요: name_type == 1 이 아닌 경우에 대해서는 사용하면 안됨.** class UserViewPage extends StatefulWidget { @@ -55,7 +53,7 @@ class _UserViewPageState extends State { final ScrollController _listViewController = ScrollController(); /// 사용자가 작성한 글 모델을 저장하는 리스트. - List _articleList = []; + final List _articleList = []; @override void initState() { @@ -148,7 +146,9 @@ class _UserViewPageState extends State { SizedBox( width: MediaQuery.of(context).size.width - 40, child: Text( - '총 $_articleCount개의 글', + context.locale == const Locale('ko') + ? '총 $_articleCount개의 글' + : '$_articleCount posts', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, @@ -249,9 +249,8 @@ class _UserViewPageState extends State { }, // 각각의 작성한 글 child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11.0), - child: PostPreview(model: curPost) - ), + padding: const EdgeInsets.symmetric(vertical: 11.0), + child: PostPreview(model: curPost)), ); }, separatorBuilder: (BuildContext context, int idx) { @@ -266,6 +265,7 @@ class _UserViewPageState extends State { } /// 글에 첨부된 파일의 타입에 따른 위젯을 리턴하는 함수. + // ignore: unused_element Widget _buildAttachImage(String attachmentType) { return Row( children: [ @@ -328,12 +328,12 @@ class _UserViewPageState extends State { /// API 통신 및 userProfileModel에 정보 저장이 모두 성공적일 경우 true. /// 아닌 경우 false 반환. Future _fetchUser(UserProvider userProvider) async { - String apiUrl = "/api/user_profiles/${widget.userID}"; + String apiUrl = "user_profiles/${widget.userID}"; try { - var response = await userProvider.createDioWithHeadersForGet().get("$newAraDefaultUrl$apiUrl"); - dynamic json = response.data; + var response = await userProvider.getApiRes(apiUrl); + final Map? json = await response?.data; try { - _userProfileModel = PublicUserProfileModel.fromJson(json); + _userProfileModel = PublicUserProfileModel.fromJson(json!); return true; } catch (error) { debugPrint("fetch user failed: ${widget.userID}"); @@ -363,15 +363,16 @@ class _UserViewPageState extends State { Future _fetchCreatedArticles( UserProvider userProvider, int page) async { int user = _userProfileModel.user; - String apiUrl = "/api/articles/?page=$page&created_by=$user"; + String apiUrl = "articles/?page=$page&created_by=$user"; // 첫 페이지일 경우 기존에 존재하는 article을 모두 제거. if (page == 1) { _articleList.clear(); _nextPage = 1; } try { - var response = await userProvider.createDioWithHeadersForGet().get("$newAraDefaultUrl$apiUrl"); - List rawPostList = response.data['results']; + var response = await userProvider.getApiRes(apiUrl); + final Map? jsonList = await response?.data; + List rawPostList = jsonList?['results']; for (int i = 0; i < rawPostList.length; i++) { Map? rawPost = rawPostList[i]; // 가끔 형식에 맞지 않은 데이터를 가진 글이 있어 추가함.(2023.05.26) @@ -387,7 +388,7 @@ class _UserViewPageState extends State { } } _nextPage += 1; - _articleCount = response.data['num_items']; + _articleCount = jsonList?['num_items']; return true; } on DioException catch (e) { // 서버에서 response를 보냈지만 invalid한 statusCode일 때 diff --git a/lib/providers/blocked_provider.dart b/lib/providers/blocked_provider.dart index c0cb623a..bc6f31a7 100644 --- a/lib/providers/blocked_provider.dart +++ b/lib/providers/blocked_provider.dart @@ -48,7 +48,7 @@ class BlockedProvider with ChangeNotifier { Future fetchBlockedAnonymousPostID() async { dynamic fetchedList = await fetchCachedApiData(blockedPostAnonymousKey); - debugPrint('fetchedList: ${fetchedList}'); + debugPrint('fetchedList: $fetchedList'); if (fetchedList == null) { _blockedAnonymousPostIDs = {}; } else { @@ -61,7 +61,7 @@ class BlockedProvider with ChangeNotifier { Future fetchBlockedAnonymousCommentID() async { dynamic fetchedList = await fetchCachedApiData(blockedCommentAnonymousKey); - debugPrint('fetchedList: ${fetchedList}'); + debugPrint('fetchedList: $fetchedList'); if (fetchedList == null) { _blockedAnonymousCommentIDs = {}; } else { diff --git a/lib/providers/notification_provider.dart b/lib/providers/notification_provider.dart index bdea248e..42dbca6f 100644 --- a/lib/providers/notification_provider.dart +++ b/lib/providers/notification_provider.dart @@ -1,16 +1,13 @@ -/// 'notification_provider.dart' -/// [NotificationProvider]를 정의함. -/// 새로운 알림이 생성되었는지 확인하고 구독 중인 리스너에게 알려주는 기능. -/// -/// Author: 김상오(alvin) +// 'notification_provider.dart' +// [NotificationProvider]를 정의함. +// 새로운 알림이 생성되었는지 확인하고 구독 중인 리스너에게 알려주는 기능. +// +// Author: 김상오(alvin) import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:dio/dio.dart'; -import 'package:new_ara_app/constants/url_info.dart'; import 'package:new_ara_app/providers/user_provider.dart'; -import 'package:provider/provider.dart'; /// 새로운 알림 여부를 알려주는 ChangeNotifier. /// 알림 생성 여부가 필요한 다양한 위젯에 활용하기 위해 ChangeNotifier로 구현함. @@ -19,6 +16,7 @@ class NotificationProvider with ChangeNotifier { bool _isNotReadExist = false; bool get isNotReadExist => _isNotReadExist; + // ignore: unused_field String _cookieString = ""; /// [checkIsNotReadExist] 속의 dio를 위한 쿠키를 설정해줌. @@ -41,8 +39,6 @@ class NotificationProvider with ChangeNotifier { Future checkIsNotReadExist(UserProvider userProvider) async { bool res = false; - Dio dio = userProvider.createDioWithHeadersForGet(); - int curPage = 1; /// 다음 페이지가 존재하는 지 나타냄. @@ -51,9 +47,11 @@ class NotificationProvider with ChangeNotifier { do { try { var response = - await dio.get("$newAraDefaultUrl/api/notifications/?page=$curPage"); - hasNext = response.data["next"] == null ? false : true; // 다음 페이지 존재 확인. - List resultsJson = response.data["results"]; + await userProvider.getApiRes("notifications/?page=$curPage"); + final Map? jsonList = await response?.data; + + hasNext = jsonList?["next"] == null ? false : true; // 다음 페이지 존재 확인. + List resultsJson = jsonList?["results"]; for (var json in resultsJson) { if (!(json['is_read'] ?? true)) { res = true; diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index af9bd5fe..3d166068 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -1,12 +1,14 @@ -import 'dart:convert'; import 'dart:io'; -import 'dart:math'; -import 'package:http/http.dart' as http; import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:new_ara_app/constants/url_info.dart'; import 'package:new_ara_app/models/user_profile_model.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/utils/create_dio_with_config.dart'; +import 'package:new_ara_app/utils/global_key.dart'; +import 'package:new_ara_app/widgets/snackbar_noti.dart'; +import 'package:path/path.dart'; import 'package:webview_cookie_manager/webview_cookie_manager.dart'; /// `UserProvider`는 사용자 정보 및 연관된 API 로직을 관리하는 클래스입니다. @@ -23,6 +25,8 @@ class UserProvider with ChangeNotifier { bool get hasData => _hasData; dynamic get apiRes => _apiRes; + bool internetConnected = true; //인터넷 연결 여부 표시 + /// `_hasData`의 값을 설정하고 UI를 업데이트합니다. void setHasData(bool tf) { _hasData = tf; @@ -131,138 +135,233 @@ class UserProvider with ChangeNotifier { return dio; } - /// 지정된 API URL로 GET 요청을 전송하고 응답의 data를 반환합니다. - /// - /// 실패 시 null을 반환합니다. - /// - /// sendText는 개발자가 디버깅을 위한 문자열입니다. - /// - /// 사용 예시: await getApiRes('unregister', sendText: '디버깅용 테스트 문자열 입니다.'); - Future getApiRes(String apiUrl, {String? sendText}) async { - var totUrl = "$newAraDefaultUrl/api/$apiUrl"; + /// get, post, patch, put 등 wrapper의 + /// error handling 방식을 통일합니다. + void dioErrorHandling(DioException e) { + late String errorMessage; + if (e.type == DioExceptionType.connectionTimeout) { + errorMessage = "DioException: Connection Time Out"; + } else if (e.type == DioExceptionType.sendTimeout) { + errorMessage = "DioException: Send Time Out"; + } else if (e.type == DioExceptionType.receiveTimeout) { + errorMessage = "DioException: Receive Time Out"; + } else if (e.type == DioExceptionType.badCertificate) { + errorMessage = "DioException: Bad Certificate"; + } else if (e.type == DioExceptionType.badResponse) { + errorMessage = "DioException: Bad Response"; + } else if (e.type == DioExceptionType.cancel) { + errorMessage = "DioException: Cancel"; + } else if (e.type == DioExceptionType.connectionError) { + //와이파이 연결에 문제가 발생할 때 connectionError가 throw됨. + errorMessage = "DioException: Connection Error"; - Dio dio = createDioWithHeadersForGet(); + //이에 따라 인터넷 에러를 표시하는 snackBar 추가 + if (internetConnected) { + // 첫 실행이라면 + internetConnected = false; // 이후 snackBar 생성하지 않음. + showInternetErrorBySnackBar(LocaleKeys.userProvider_internetError.tr()); + } + } else if (e.type == DioExceptionType.unknown) { + errorMessage = "DioException: Unknown: ${e.message}"; + } else { + // 이 case는 이론상 없어야 함. 추후 DioExceptionType이 dio package 버전에 따라 변경되었을 때를 대비해 넣어둠. + errorMessage = "DioExceptionType enum에 정의되어있지 않은 오류 발생"; + } + debugPrint(errorMessage); + if (e.response != null) { + //debugPrint("${e.response!.data}"); + debugPrint("${e.response!.headers}"); + debugPrint("${e.response!.requestOptions}"); + } + // request의 setting, sending에서 문제 발생 + // requestOption, message를 출력. + else { + debugPrint("${e.requestOptions}"); + debugPrint("${e.message}"); + } + } - late dynamic response; + /// 지정된 API URL로 GET 요청을 전송하고 응답의 data를 반환합니다. + /// + /// 실패 시 null을 return하고, 오류 메시지를 debugPrint합니다. + // + /// path는 API 주소입니다. + /// + /// 사용 예시: await getApiRes('unregister', queryParameters:queryParameters); + Future?> getApiRes( + String path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, + }) async { + var toUrl = "$newAraDefaultUrl/api/$path"; + Dio dio = createDioWithHeadersForGet(); try { - response = await dio.get(totUrl); + final response = await dio.get( + toUrl, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ); + internetConnected = true; + + //인터넷 오류 snackBar 모두 지우기 + snackBarKey.currentState?.clearSnackBars(); + + return response; } on DioException catch (e) { - debugPrint("getApiRes failed with DioException: $e"); - // 서버에서 response를 보냈지만 invalid한 statusCode일 때 - if (e.response != null) { - debugPrint("${e.response!.data}"); - debugPrint("${e.response!.headers}"); - debugPrint("${e.response!.requestOptions}"); - } - // request의 setting, sending에서 문제 발생 - // requestOption, message를 출력. - else { - debugPrint("${e.requestOptions}"); - debugPrint("${e.message}"); - } + debugPrint("Error occured in fetching : $toUrl"); + dioErrorHandling(e); return null; } catch (e) { - debugPrint("_fetchUser failed with error: $e"); + debugPrint("오류 발생: ${e.toString()}"); + debugPrint("Error occured in fetching : $toUrl"); return null; + //throw Exception("Non-DioException occurred: ${e.toString()}"); } - return response.data; } - Future postApiRes(String apiUrl, {dynamic payload}) async { - String totUrl = "$newAraDefaultUrl/api/$apiUrl"; - + /// path에 주어진 경로와 data, queryParameters를 이용해 POST 요청을 보냄. + /// 성공하면 Response 객체를 반환. + /// 실패하면 내부에서 exception handling한 이후 null을 반환. + Future?> postApiRes(String path, + {Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress}) async { + String toUrl = "$newAraDefaultUrl/api/$path"; Dio dio = createDioWithHeadersForNonget(); - dio.options.headers['Cookie'] = getCookiesToString(); - dio.options.headers['X-Csrftoken'] = getCsrftokenToString(); - - late dynamic response; try { - response = await dio.post(totUrl, data: payload); + final response = await dio.post(toUrl, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress); + internetConnected = true; + + //인터넷 오류 snackBar 모두 지우기 + snackBarKey.currentState?.clearSnackBars(); + return response; } on DioException catch (e) { - debugPrint("getApiRes failed with DioException: $e"); - // 서버에서 response를 보냈지만 invalid한 statusCode일 때 - if (e.response != null) { - debugPrint("${e.response!.data}"); - debugPrint("${e.response!.headers}"); - debugPrint("${e.response!.requestOptions}"); - return e.response; - } - // request의 setting, sending에서 문제 발생 - // requestOption, message를 출력. - else { - debugPrint("${e.requestOptions}"); - debugPrint("${e.message}"); - return null; - } + debugPrint("Error occured in fetching : $toUrl"); + dioErrorHandling(e); + return null; } catch (e) { - debugPrint("_fetchUser failed with error: $e"); + debugPrint("오류 발생: ${e.toString()}"); return null; } - return response; } - Future delApiRes(String apiUrl, {dynamic payload}) async { - String totUrl = "$newAraDefaultUrl/api/$apiUrl"; - + /// path에 주어진 경로와 data, queryParameters를 이용해 PUT 요청을 보냄. + /// 성공하면 Response 객체를 반환. + /// 실패하면 내부에서 exception handling한 이후 null을 반환. + Future?> putApiRes(String path, + {Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress}) async { + String toUrl = "$newAraDefaultUrl/api/$path"; Dio dio = createDioWithHeadersForNonget(); - dio.options.headers['Cookie'] = getCookiesToString(); - dio.options.headers['X-Csrftoken'] = getCsrftokenToString(); - - late dynamic response; try { - response = await dio.delete(totUrl, data: payload); + final response = await dio.put(toUrl, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress); + internetConnected = true; + + //인터넷 오류 snackBar 모두 지우기 + snackBarKey.currentState?.clearSnackBars(); + return response; } on DioException catch (e) { - debugPrint("getApiRes failed with DioException: $e"); - // 서버에서 response를 보냈지만 invalid한 statusCode일 때 - if (e.response != null) { - debugPrint("${e.response!.data}"); - debugPrint("${e.response!.headers}"); - debugPrint("${e.response!.requestOptions}"); - return e.response; - } - // request의 setting, sending에서 문제 발생 - // requestOption, message를 출력. - else { - debugPrint("${e.requestOptions}"); - debugPrint("${e.message}"); - return null; - } + debugPrint("Error occured in fetching : $toUrl"); + dioErrorHandling(e); + return null; } catch (e) { - debugPrint("_fetchUser failed with error: $e"); + debugPrint("오류 발생: ${e.toString()}"); return null; } - return response; } - Future patchApiRes(String apiUrl, {dynamic payload}) async { - String totUrl = "$newAraDefaultUrl/api/$apiUrl"; - + /// path에 주어진 경로와 data, queryParameters를 이용해 PATCH 요청을 보냄. + /// 성공하면 Response 객체를 반환. + /// 실패하면 내부에서 exception handling한 이후 null을 반환. + Future?> patchApiRes(String path, + {Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress}) async { + String toUrl = "$newAraDefaultUrl/api/$path"; Dio dio = createDioWithHeadersForNonget(); - dio.options.headers['Cookie'] = getCookiesToString(); - dio.options.headers['X-Csrftoken'] = getCsrftokenToString(); + try { + final response = await dio.patch(toUrl, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress); + internetConnected = true; + + //인터넷 오류 snackBar 모두 지우기 + snackBarKey.currentState?.clearSnackBars(); + return response; + } on DioException catch (e) { + debugPrint("Error occured in fetching : $toUrl"); + dioErrorHandling(e); + return null; + } catch (e) { + debugPrint("오류 발생: ${e.toString()}"); + return null; + } + } - late Response response; + /// path에 주어진 경로와 data, queryParameters를 이용해 DELETE 요청을 보냄. + /// 성공하면 Response 객체를 반환. + /// 실패하면 내부에서 exception handling한 이후 null을 반환. + Future?> delApiRes( + String path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + String toUrl = "$newAraDefaultUrl/api/$path"; + Dio dio = createDioWithHeadersForNonget(); try { - response = await dio.patch(totUrl, data: payload); + final response = await dio.delete( + toUrl, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + internetConnected = true; + + //인터넷 오류 snackBar 모두 지우기 + snackBarKey.currentState?.clearSnackBars(); + return response; } on DioException catch (e) { - debugPrint("getApiRes failed with DioException: $e"); - // 서버에서 response를 보냈지만 invalid한 statusCode일 때 - if (e.response != null) { - debugPrint("${e.response!.data}"); - debugPrint("${e.response!.headers}"); - debugPrint("${e.response!.requestOptions}"); - } - // request의 setting, sending에서 문제 발생 - // requestOption, message를 출력. - else { - debugPrint("${e.requestOptions}"); - debugPrint("${e.message}"); - } - return e.response; + debugPrint("Error occured in fetching : $toUrl"); + dioErrorHandling(e); + return null; } catch (e) { - debugPrint("_fetchUser failed with error: $e"); + debugPrint("오류 발생: ${e.toString()}"); return null; } - return response; } } diff --git a/lib/translations/codegen_loader.g.dart b/lib/translations/codegen_loader.g.dart new file mode 100644 index 00000000..15fac217 --- /dev/null +++ b/lib/translations/codegen_loader.g.dart @@ -0,0 +1,418 @@ +// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart + +// ignore_for_file: prefer_single_quotes + +import 'dart:ui'; + +import 'package:easy_localization/easy_localization.dart' show AssetLoader; + +class CodegenLoader extends AssetLoader{ + const CodegenLoader(); + + @override + Future?> load(String path, Locale locale) { + return Future.value(mapLocales[locale.toString()]); + } + + static const Map en = { + "boardListPage": { + "boards": "Boards", + "searchBoardsPostsComments": "Search boards, posts, and comments", + "viewAll": "View All", + "topPosts": "Top Posts", + "bookmarks": "Bookmarks" + }, + "bulletinSearchPage": { + "searchIn": "Search in {en_name}", + "searchInAllPosts": "Search in all posts", + "searchInHistory": "Search in recently viewed posts", + "searchInTopPosts": "Search in real-time popular posts", + "searchInBookmarks": "Search in saved posts", + "search": "Search", + "pleaseEnter": "Please enter a search term", + "noResults": "No results found." + }, + "inquiryPage": { + "title": "Inquiries and Suggestions", + "reLoginErrorWithWithdrawalGuide": "This account has already been deactivated.\nIf you wish to re-register, please click this link to contact us via email.", + "reLoginErrorWithWithdrawalEmailTitle": "Re-registration Inquiry", + "reLoginErrorWithWithdrawalEmailContents": "If you have any additional comments, please write them below.\n\n※ An Ara administrator will respond within 48 hours.※\n\nUser ID: {userID}\nNickname: {nickname}\nEmail: {email}\nPlatform: App\n" + }, + "loginPage": { + "login": "Sign in with SPARCS SSO" + }, + "mainPage": { + "topPost": "Top Posts", + "notice": "Notice", + "portalNotice": "Portal Notice", + "facility": "Facility", + "araAdmins": "Ara Admins", + "trades": "Trades", + "realEstate": "Real Estate", + "market": "Market", + "jobsWanted": "Jobs Wanted", + "organizationsAndClubs": "Organizations and Clubs", + "gradAssoc": "Grad Assoc", + "undergradAssoc": "Undergrad Assoc", + "freshmanCouncil": "Freshman Council", + "talk": "Talk" + }, + "notificationPage": { + "notifications": "Notifications", + "noNotifications": "No notifications.", + "today": "Today", + "post": "Post", + "newComment": "New comment to your post.", + "allNotificationsChecked": "You've already checked all the notifications." + }, + "postListShowPage": { + "boards": "Boards", + "history": "History", + "topPosts": "Top Posts", + "allPosts": "All Posts", + "bookmarks": "Bookmarks", + "testBoard": "Test Board" + }, + "settingPage": { + "title": "Setting", + "postSetting": "Post Setting", + "adult": "Allow Access to Adult Posts", + "politics": "Allow Access to Political Posts", + "block": "Blocked Users", + "viewBlockedUsers": "View Blocked Users", + "howToBlock": "You can block accounts through the detail function in posts or comments.", + "information": "Information", + "termsAndConditions": "Terms and Conditions of Service", + "contactAdmins": "Contact the Admins", + "signOut": "Sign out", + "withdrawal": "Withdrawal", + "withdrawalGuide": "Your request for account deletion will be processed after confirmation by the Ara administrator, and it may take up to 24 hours.", + "userBlockingGuide": "User blocking can be done through the 'More' option in posts. You can change it up to 10 times per day.", + "settingsSaved": "The settings have been saved.", + "errorSavingSettings": "An error occurred, and the settings could not be applied. Please try again.", + "myReplies": "Comments and Nested Comments of My Posts", + "replies": "Nested Comments", + "hotNotifications": "Hot Notifications", + "hotPosts": "Hot Posts", + "hotInfo": "We deliver trending announcements and posts every day at 8:30 a.m.", + "emailNotAvailable": "If the default mail application cannot be opened, please contact ara@sparcs.org." + }, + "userPage": { + "change": "Chg.", + "myPosts": "My Posts", + "bookmarks": "Bookmarks", + "history": "History", + "totalNPosts": "{curCount} posts", + "noEmailInfo": "No email information" + }, + "postViewPage": { + "reply": "Reply", + "scrap": "To Bookmark", + "scrapped": "Bookmarked", + "share": "Share", + "launchInBrowserNotAvailable": "The URL could not be opened by browser.", + "showHiddenPosts": "Show hidden posts", + "hit": "hits", + "noSelfVotingInfo": "You cannot vote for your post or comment!", + "failedToBlock": "Failed to block.", + "unblock": "Unblock", + "block": "Block", + "delete": "Delete", + "report": "Report", + "edit": "Edit", + "commentHintText": "Type your comment here.", + "noCommentWarning": "No comment has been written!", + "displayCommentCount": " comments", + "copyLinkToClipBoard": "Copied URL to the clipboard.", + "blockedUsersPost": "This post was written by blocked user.", + "blockedUsersComment": "This comment was written by blocked user.", + "reportedPost": "This post is hidden due to the accumulation of reports.", + "reportedComment": "This comment is hidden due to the accumulation of reports.", + "adultPost": "This post has adult/obscene contents.", + "socialPost": "This post has political/social contents.", + "accessDeniedPost": "Access for this post is denied.", + "hiddenPost": "This post is hidden.", + "deletedComment": "This comment is deleted.", + "hiddenComment": "This comment is hidden.", + "blockedUsersContentNotice": "You can change blocked users in your Setting page.", + "adultContentNotice": "You can change the setting in your Setting page to show this kinds of post.", + "socialContentNotice": "You can change the setting in your Setting page to show this kinds of post." + }, + "postViewUtils": { + "letUsKnowPostReportReason": "Let us know your reason for reporting the post.", + "letUsKnowCommentReportReason": "Let us know your reason for reporting the comment.", + "reportPostSucceed": "Post is successfully reported.", + "alreadyReported": "You've already reported this post.", + "reportButton": "Report", + "cancel": "Cancel" + }, + "dialogs": { + "deleteConfirm": "Do you really want to delete this post?", + "blockConfirm": "Do you really want to block this user?", + "logoutConfirm": "Do you really want to sign out?", + "withdrawalConfirm": "Do you really want to withdraw the membership?", + "withdrawalEmailInfo": "If you leave the membership, you can't re-sign up with the email you're using now", + "cancel": "Cancel", + "confirm": "OK", + "noBlockedUsers": "There are no blocked users.", + "noNickname": "No nickname" + }, + "popUpMenuButtons": { + "downloadSucceed": "File downloaded successfully", + "downloadFailed": "Downloading File failed", + "attachments": "Attachments", + "report": "Report", + "edit": "Edit", + "delete": "Delete", + "withSchoolInfoText": "This board is for students to freely share their opinions with the school. Any post with over 20 votes will get an official reply from the school. Please be aware that all posts here are made with real names, for clear and responsible communication. " + }, + "postPreview": { + "blockedUsersPost": "This post was written by blocked user.", + "reportedPost": "This post is hidden due to the accumulation of reports.", + "adultPost": "This post has adult/obscene contents.", + "socialPost": "This post has political/social contents.", + "accessDeniedPost": "Access for this post is denied.", + "hiddenPost": "This post is hidden.", + "beforeUpVoteThreshold": "Polling", + "beforeSchoolConfirm": "Preparing", + "answerDone": "Answered" + }, + "profileEditPage": { + "settingInfoText": "There was a problem changing the settings.", + "editProfile": "Edit Profile", + "complete": "Complete", + "nickname": "Nickname", + "email": "Email", + "noEmail": "Email doesn't exist", + "nicknameInfo": "Once you change your nickname, you can't change it for three months.", + "nicknameHintText": "Please enter the nickname you want to change to.", + "nicknameEmptyInfo": "Nickname not provided!" + }, + "postWritePage": { + "write": "Write a post", + "submit": "Submit", + "titleHintText": "Type title here", + "selectBoard": "Select Board", + "addAttach": "Upload Attachments", + "attachments": "Attachments", + "terms": "Terms", + "realNameNotice": "This post will be under your real name.", + "anonymous": "Anon", + "adult": "Adult", + "politics": "Politics", + "contentPlaceholder": "Type content here", + "conditionSnackBar": "Please select a board and enter the title and content.", + "noCategory": "No Topics", + "selectCategory": "Select Topic" + }, + "termsAndConditionsPage": { + "termsAndConditions": "Terms and Conditions", + "agree": "Agree", + "agreed": "You've already agreed." + }, + "userProvider": { + "internetError": "Please check your network." + } +}; +static const Map ko = { + "boardListPage": { + "boards": "게시판", + "searchBoardsPostsComments": "게시판, 게시글 및 댓글 검색", + "viewAll": "전체보기", + "topPosts": "인기글", + "bookmarks": "담아둔 글" + }, + "bulletinSearchPage": { + "searchIn": "{ko_name}에서 검색", + "searchInAllPosts": "전체 보기에서 검색", + "searchInHistory": "최근 본 글에서 검색", + "searchInTopPosts": "실시간 인기글에서 검색", + "searchInBookmarks": "담아둔 글에서 검색", + "pleaseEnter": "검색어를 입력하세요", + "search": "검색", + "noResults": "검색 결과가 없습니다." + }, + "inquiryPage": { + "title": "문의 및 건의", + "reLoginErrorWithWithdrawalGuide": "이미 탈퇴 했던 계정입니다.\n재가입을 하고 싶다면 이 링크를 눌러 이메일로 문의하세요.", + "reLoginErrorWithWithdrawalEmailTitle": "재가입 문의", + "reLoginErrorWithWithdrawalEmailContents": "추가로 말하실 말이 있다면 여기 아래에 적어주세요.\n\n※ Ara 관리자가 48시간 이내로 답변드립니다.※\n\n유저 번호: {userID}\n닉네임: {nickname}\n이메일: {email}\n플랫폼: App\n" + }, + "loginPage": { + "login": "SPARCS SSO로 로그인" + }, + "mainPage": { + "topPost": "실시간 인기글", + "notice": "공지", + "portalNotice": "포탈 공지", + "facility": "입주 업체", + "araAdmins": "Ara 운영진", + "trades": "거래", + "realEstate": "부동산", + "market": "중고거래", + "jobsWanted": "구인구직", + "organizationsAndClubs": "학생 단체", + "gradAssoc": "원총", + "undergradAssoc": "총학", + "freshmanCouncil": "새학", + "talk": "자유게시판" + }, + "notificationPage": { + "notifications": "알림", + "noNotifications": "알림이 없습니다.", + "today": "오늘", + "post": "게시물", + "newComment": "회원님의 게시물에 새로운 댓글이 작성되었습니다.", + "allNotificationsChecked": "이미 알림을 모두 읽으셨습니다." + }, + "postListShowPage": { + "boards": "게시판", + "history": "최근 본 글", + "topPosts": "실시간 인기글", + "allPosts": "전체보기", + "bookmarks": "담아둔 글", + "testBoard": "테스트 게시판" + }, + "settingPage": { + "title": "설정", + "postSetting": "게시글 설정", + "adult": "성인글 보기", + "politics": "정치글 보기", + "block": "차단", + "viewBlockedUsers": "차단한 유저 목록", + "howToBlock": "유저 차단은 게시글이나 댓글에서 더보기 기능을 통해 가능합니다.", + "information": "정보", + "termsAndConditions": "이용약관", + "contactAdmins": "운영진에게 문의하기", + "signOut": "로그아웃", + "withdrawal": "회원탈퇴", + "withdrawalGuide": "회원 탈퇴는 Ara 관리자가 확인 후 처리해드리며, 최대 24시간이 소요될 수 있습니다.", + "userBlockingGuide": "유저 차단은 게시글의 '더보기' 기능에서 하실 수 있습니다. 하루에 최대 10번만 변경 가능합니다.", + "settingsSaved": "설정이 저장되었습니다.", + "errorSavingSettings": "에러가 발생하여 설정 반영에 실패했습니다. 다시 시도해주십시오.", + "myReplies": "내 글에 달린 댓글 및 대댓글", + "replies": "댓글에 달린 대댓글", + "hotNotifications": "인기 공지글", + "hotPosts": "인기글", + "hotInfo": "인기 공지글 및 인기 글을 매일 오전 8시 30분에 전달해 드립니다.", + "emailNotAvailable": "기본 메일 어플리케이션을 열 수 없습니다. ara@sparcs.org로 문의 부탁드립니다." + }, + "userPage": { + "change": "수정", + "myPosts": "작성한 글", + "bookmarks": "담아둔 글", + "history": "최근 본 글", + "totalNPosts": "총 {curCount}개의 글", + "noEmailInfo": "이메일 정보가 없습니다." + }, + "postViewPage": { + "reply": "답글 쓰기", + "scrap": "담아두기", + "scrapped": "담아둔 글", + "share": "공유", + "launchInBrowserNotAvailable": "브라우저로 URL을 열 수 없습니다.", + "showHiddenPosts": "숨긴내용 보기", + "hit": "조회", + "noSelfVotingInfo": "본인 게시글이나 댓글에는 좋아요를 누를 수 없습니다.", + "failedToBlock": "차단에 실패했습니다.", + "unblock": "차단 해제", + "block": "차단", + "delete": "삭제", + "report": "신고", + "edit": "수정", + "commentHintText": "댓글을 입력해주세요.", + "noCommentWarning": "댓글이 작성되지 않았습니다!", + "displayCommentCount": "개의 댓글", + "copyLinkToClipBoard": "URL을 클립 보드에 복사했습니다.", + "blockedUsersPost": "차단한 사용자의 게시물입니다.", + "blockedUsersComment": "차단한 사용자의 댓글입니다.", + "reportedPost": "신고 누적으로 숨김된 게시물입니다.", + "reportedComment": "신고 누적으로 숨김된 댓글입니다.", + "adultPost": "성인/음란성 내용의 게시물입니다.", + "socialPost": "정치/사회성 내용의 게시물입니다.", + "accessDeniedPost": "접근 권한이 없는 게시물입니다.", + "hiddenPost": "숨겨진 게시물입니다.", + "deletedComment": "삭제된 댓글입니다.", + "hiddenComment": "숨겨진 댓글입니다.", + "blockedUsersContentNotice": "차단 사용자 설정은 설정페이지에서 수정할 수 있습니다.", + "adultContentNotice": "게시글 보기 설정은 설정페이지에서 수정할 수 있습니다.", + "socialContentNotice": "게시글 보기 설정은 설정페이지에서 수정할 수 있습니다." + }, + "postViewUtils": { + "letUsKnowPostReportReason": "게시글 신고 사유를 알려주세요.", + "letUsKnowCommentReportReason": "댓글 신고 사유를 알려주세요.", + "reportPostSucceed": "해당 게시글을 신고했습니다.", + "alreadyReported": "이미 신고한 게시글입니다.", + "reportButton": "신고하기", + "cancel": "취소" + }, + "dialogs": { + "deleteConfirm": "정말로 삭제하시겠습니까?", + "blockConfirm": "정말로 차단하시겠습니까?", + "logoutConfirm": "정말로 로그아웃 하시겠습니까?", + "withdrawalConfirm": "정말로 회원탈퇴 하시겠습니까?", + "withdrawalEmailInfo": "회원탈퇴하시면 지금 쓰시는 이메일로는 재가입이 불가능합니다", + "cancel": "취소", + "confirm": "확인", + "noBlockedUsers": "차단한 유저가 없습니다.", + "noNickname": "닉네임이 없음" + }, + "popUpMenuButtons": { + "downloadSucceed": "파일 다운로드에 성공했습니다", + "downloadFailed": "파일 다운로드에 실패했습니다", + "attachments": "첨부파일 모아보기", + "report": "신고", + "edit": "수정", + "delete": "삭제", + "withSchoolInfoText": "본 게시판은 교내 구성원들이 실명으로 학교에 의견을 제시하는 게시판이며, 좋아요 수가 20개 이상인 모든 글에 대해 학교 측 공식 답변을 받으실 수 있습니다. 투명하고 책임감 있는 의견 공유를 위해 댓글 작성 시 실명으로 공개됩니다." + }, + "postPreview": { + "blockedUsersPost": "차단한 사용자의 게시물입니다.", + "reportedPost": "신고 누적으로 숨김된 게시물입니다.", + "adultPost": "성인/음란성 내용의 게시물입니다.", + "socialPost": "정치/사회성 내용의 게시물입니다.", + "accessDeniedPost": "접근 권한이 없는 게시물입니다.", + "hiddenPost": "숨겨진 게시물입니다.", + "beforeUpVoteThreshold": "달성 전", + "beforeSchoolConfirm": "답변 대기 중", + "answerDone": "답변 완료" + }, + "profileEditPage": { + "settingInfoText": "설정 변경 중 문제가 발생했습니다.", + "editProfile": "프로필 수정", + "complete": "완료", + "nickname": "닉네임", + "email": "이메일", + "noEmail": "이메일 정보가 없습니다.", + "nicknameInfo": "닉네임은 한번 변경할 시 3개월간 변경이 불가합니다.", + "nicknameHintText": "변경하실 닉네임을 입력해주세요.", + "nicknameEmptyInfo": "닉네임이 작성되지 않았습니다!" + }, + "postWritePage": { + "write": "글 쓰기", + "submit": "올리기", + "titleHintText": "제목을 입력해주세요.", + "selectBoard": "게시판을 선택하세요", + "addAttach": "첨부파일 추가", + "attachments": "첨부파일", + "terms": "이용약관", + "realNameNotice": "이 게시물은 실명으로 게시됩니다.", + "anonymous": "익명", + "adult": "성인", + "politics": "정치", + "contentPlaceholder": "내용을 입력해주세요.", + "conditionSnackBar": "게시판을 선택해주시고 제목, 내용을 입력해주세요.", + "noCategory": "말머리 없음", + "selectCategory": "말머리를 선택하세요" + }, + "termsAndConditionsPage": { + "termsAndConditions": "이용약관", + "agree": "동의 하기", + "agreed": "이미 동의하셨습니다." + }, + "userProvider": { + "internetError": "인터넷 오류가 발생하였습니다." + } +}; +static const Map> mapLocales = {"en": en, "ko": ko}; +} diff --git a/lib/translations/locale_keys.g.dart b/lib/translations/locale_keys.g.dart new file mode 100644 index 00000000..87972086 --- /dev/null +++ b/lib/translations/locale_keys.g.dart @@ -0,0 +1,185 @@ +// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart + +abstract class LocaleKeys { + static const boardListPage_boards = 'boardListPage.boards'; + static const boardListPage_searchBoardsPostsComments = 'boardListPage.searchBoardsPostsComments'; + static const boardListPage_viewAll = 'boardListPage.viewAll'; + static const boardListPage_topPosts = 'boardListPage.topPosts'; + static const boardListPage_bookmarks = 'boardListPage.bookmarks'; + static const boardListPage = 'boardListPage'; + static const bulletinSearchPage_searchIn = 'bulletinSearchPage.searchIn'; + static const bulletinSearchPage_searchInAllPosts = 'bulletinSearchPage.searchInAllPosts'; + static const bulletinSearchPage_searchInHistory = 'bulletinSearchPage.searchInHistory'; + static const bulletinSearchPage_searchInTopPosts = 'bulletinSearchPage.searchInTopPosts'; + static const bulletinSearchPage_searchInBookmarks = 'bulletinSearchPage.searchInBookmarks'; + static const bulletinSearchPage_search = 'bulletinSearchPage.search'; + static const bulletinSearchPage_pleaseEnter = 'bulletinSearchPage.pleaseEnter'; + static const bulletinSearchPage_noResults = 'bulletinSearchPage.noResults'; + static const bulletinSearchPage = 'bulletinSearchPage'; + static const inquiryPage_title = 'inquiryPage.title'; + static const inquiryPage_reLoginErrorWithWithdrawalGuide = 'inquiryPage.reLoginErrorWithWithdrawalGuide'; + static const inquiryPage_reLoginErrorWithWithdrawalEmailTitle = 'inquiryPage.reLoginErrorWithWithdrawalEmailTitle'; + static const inquiryPage_reLoginErrorWithWithdrawalEmailContents = 'inquiryPage.reLoginErrorWithWithdrawalEmailContents'; + static const inquiryPage = 'inquiryPage'; + static const loginPage_login = 'loginPage.login'; + static const loginPage = 'loginPage'; + static const mainPage_topPost = 'mainPage.topPost'; + static const mainPage_notice = 'mainPage.notice'; + static const mainPage_portalNotice = 'mainPage.portalNotice'; + static const mainPage_facility = 'mainPage.facility'; + static const mainPage_araAdmins = 'mainPage.araAdmins'; + static const mainPage_trades = 'mainPage.trades'; + static const mainPage_realEstate = 'mainPage.realEstate'; + static const mainPage_market = 'mainPage.market'; + static const mainPage_jobsWanted = 'mainPage.jobsWanted'; + static const mainPage_organizationsAndClubs = 'mainPage.organizationsAndClubs'; + static const mainPage_gradAssoc = 'mainPage.gradAssoc'; + static const mainPage_undergradAssoc = 'mainPage.undergradAssoc'; + static const mainPage_freshmanCouncil = 'mainPage.freshmanCouncil'; + static const mainPage_talk = 'mainPage.talk'; + static const mainPage = 'mainPage'; + static const notificationPage_notifications = 'notificationPage.notifications'; + static const notificationPage_noNotifications = 'notificationPage.noNotifications'; + static const notificationPage_today = 'notificationPage.today'; + static const notificationPage_post = 'notificationPage.post'; + static const notificationPage_newComment = 'notificationPage.newComment'; + static const notificationPage_allNotificationsChecked = 'notificationPage.allNotificationsChecked'; + static const notificationPage = 'notificationPage'; + static const postListShowPage_boards = 'postListShowPage.boards'; + static const postListShowPage_history = 'postListShowPage.history'; + static const postListShowPage_topPosts = 'postListShowPage.topPosts'; + static const postListShowPage_allPosts = 'postListShowPage.allPosts'; + static const postListShowPage_bookmarks = 'postListShowPage.bookmarks'; + static const postListShowPage_testBoard = 'postListShowPage.testBoard'; + static const postListShowPage = 'postListShowPage'; + static const settingPage_title = 'settingPage.title'; + static const settingPage_postSetting = 'settingPage.postSetting'; + static const settingPage_adult = 'settingPage.adult'; + static const settingPage_politics = 'settingPage.politics'; + static const settingPage_block = 'settingPage.block'; + static const settingPage_viewBlockedUsers = 'settingPage.viewBlockedUsers'; + static const settingPage_howToBlock = 'settingPage.howToBlock'; + static const settingPage_information = 'settingPage.information'; + static const settingPage_termsAndConditions = 'settingPage.termsAndConditions'; + static const settingPage_contactAdmins = 'settingPage.contactAdmins'; + static const settingPage_signOut = 'settingPage.signOut'; + static const settingPage_withdrawal = 'settingPage.withdrawal'; + static const settingPage_withdrawalGuide = 'settingPage.withdrawalGuide'; + static const settingPage_userBlockingGuide = 'settingPage.userBlockingGuide'; + static const settingPage_settingsSaved = 'settingPage.settingsSaved'; + static const settingPage_errorSavingSettings = 'settingPage.errorSavingSettings'; + static const settingPage_myReplies = 'settingPage.myReplies'; + static const settingPage_replies = 'settingPage.replies'; + static const settingPage_hotNotifications = 'settingPage.hotNotifications'; + static const settingPage_hotPosts = 'settingPage.hotPosts'; + static const settingPage_hotInfo = 'settingPage.hotInfo'; + static const settingPage_emailNotAvailable = 'settingPage.emailNotAvailable'; + static const settingPage = 'settingPage'; + static const userPage_change = 'userPage.change'; + static const userPage_myPosts = 'userPage.myPosts'; + static const userPage_bookmarks = 'userPage.bookmarks'; + static const userPage_history = 'userPage.history'; + static const userPage_totalNPosts = 'userPage.totalNPosts'; + static const userPage_noEmailInfo = 'userPage.noEmailInfo'; + static const userPage = 'userPage'; + static const postViewPage_reply = 'postViewPage.reply'; + static const postViewPage_scrap = 'postViewPage.scrap'; + static const postViewPage_scrapped = 'postViewPage.scrapped'; + static const postViewPage_share = 'postViewPage.share'; + static const postViewPage_launchInBrowserNotAvailable = 'postViewPage.launchInBrowserNotAvailable'; + static const postViewPage_showHiddenPosts = 'postViewPage.showHiddenPosts'; + static const postViewPage_hit = 'postViewPage.hit'; + static const postViewPage_noSelfVotingInfo = 'postViewPage.noSelfVotingInfo'; + static const postViewPage_failedToBlock = 'postViewPage.failedToBlock'; + static const postViewPage_unblock = 'postViewPage.unblock'; + static const postViewPage_block = 'postViewPage.block'; + static const postViewPage_delete = 'postViewPage.delete'; + static const postViewPage_report = 'postViewPage.report'; + static const postViewPage_edit = 'postViewPage.edit'; + static const postViewPage_commentHintText = 'postViewPage.commentHintText'; + static const postViewPage_noCommentWarning = 'postViewPage.noCommentWarning'; + static const postViewPage_displayCommentCount = 'postViewPage.displayCommentCount'; + static const postViewPage_copyLinkToClipBoard = 'postViewPage.copyLinkToClipBoard'; + static const postViewPage_blockedUsersPost = 'postViewPage.blockedUsersPost'; + static const postViewPage_blockedUsersComment = 'postViewPage.blockedUsersComment'; + static const postViewPage_reportedPost = 'postViewPage.reportedPost'; + static const postViewPage_reportedComment = 'postViewPage.reportedComment'; + static const postViewPage_adultPost = 'postViewPage.adultPost'; + static const postViewPage_socialPost = 'postViewPage.socialPost'; + static const postViewPage_accessDeniedPost = 'postViewPage.accessDeniedPost'; + static const postViewPage_hiddenPost = 'postViewPage.hiddenPost'; + static const postViewPage_deletedComment = 'postViewPage.deletedComment'; + static const postViewPage_hiddenComment = 'postViewPage.hiddenComment'; + static const postViewPage_blockedUsersContentNotice = 'postViewPage.blockedUsersContentNotice'; + static const postViewPage_adultContentNotice = 'postViewPage.adultContentNotice'; + static const postViewPage_socialContentNotice = 'postViewPage.socialContentNotice'; + static const postViewPage = 'postViewPage'; + static const postViewUtils_letUsKnowPostReportReason = 'postViewUtils.letUsKnowPostReportReason'; + static const postViewUtils_letUsKnowCommentReportReason = 'postViewUtils.letUsKnowCommentReportReason'; + static const postViewUtils_reportPostSucceed = 'postViewUtils.reportPostSucceed'; + static const postViewUtils_alreadyReported = 'postViewUtils.alreadyReported'; + static const postViewUtils_reportButton = 'postViewUtils.reportButton'; + static const postViewUtils_cancel = 'postViewUtils.cancel'; + static const postViewUtils = 'postViewUtils'; + static const dialogs_deleteConfirm = 'dialogs.deleteConfirm'; + static const dialogs_blockConfirm = 'dialogs.blockConfirm'; + static const dialogs_logoutConfirm = 'dialogs.logoutConfirm'; + static const dialogs_withdrawalConfirm = 'dialogs.withdrawalConfirm'; + static const dialogs_withdrawalEmailInfo = 'dialogs.withdrawalEmailInfo'; + static const dialogs_cancel = 'dialogs.cancel'; + static const dialogs_confirm = 'dialogs.confirm'; + static const dialogs_noBlockedUsers = 'dialogs.noBlockedUsers'; + static const dialogs_noNickname = 'dialogs.noNickname'; + static const dialogs = 'dialogs'; + static const popUpMenuButtons_downloadSucceed = 'popUpMenuButtons.downloadSucceed'; + static const popUpMenuButtons_downloadFailed = 'popUpMenuButtons.downloadFailed'; + static const popUpMenuButtons_attachments = 'popUpMenuButtons.attachments'; + static const popUpMenuButtons_report = 'popUpMenuButtons.report'; + static const popUpMenuButtons_edit = 'popUpMenuButtons.edit'; + static const popUpMenuButtons_delete = 'popUpMenuButtons.delete'; + static const popUpMenuButtons_withSchoolInfoText = 'popUpMenuButtons.withSchoolInfoText'; + static const popUpMenuButtons = 'popUpMenuButtons'; + static const postPreview_blockedUsersPost = 'postPreview.blockedUsersPost'; + static const postPreview_reportedPost = 'postPreview.reportedPost'; + static const postPreview_adultPost = 'postPreview.adultPost'; + static const postPreview_socialPost = 'postPreview.socialPost'; + static const postPreview_accessDeniedPost = 'postPreview.accessDeniedPost'; + static const postPreview_hiddenPost = 'postPreview.hiddenPost'; + static const postPreview_beforeUpVoteThreshold = 'postPreview.beforeUpVoteThreshold'; + static const postPreview_beforeSchoolConfirm = 'postPreview.beforeSchoolConfirm'; + static const postPreview_answerDone = 'postPreview.answerDone'; + static const postPreview = 'postPreview'; + static const profileEditPage_settingInfoText = 'profileEditPage.settingInfoText'; + static const profileEditPage_editProfile = 'profileEditPage.editProfile'; + static const profileEditPage_complete = 'profileEditPage.complete'; + static const profileEditPage_nickname = 'profileEditPage.nickname'; + static const profileEditPage_email = 'profileEditPage.email'; + static const profileEditPage_noEmail = 'profileEditPage.noEmail'; + static const profileEditPage_nicknameInfo = 'profileEditPage.nicknameInfo'; + static const profileEditPage_nicknameHintText = 'profileEditPage.nicknameHintText'; + static const profileEditPage_nicknameEmptyInfo = 'profileEditPage.nicknameEmptyInfo'; + static const profileEditPage = 'profileEditPage'; + static const postWritePage_write = 'postWritePage.write'; + static const postWritePage_submit = 'postWritePage.submit'; + static const postWritePage_titleHintText = 'postWritePage.titleHintText'; + static const postWritePage_selectBoard = 'postWritePage.selectBoard'; + static const postWritePage_addAttach = 'postWritePage.addAttach'; + static const postWritePage_attachments = 'postWritePage.attachments'; + static const postWritePage_terms = 'postWritePage.terms'; + static const postWritePage_realNameNotice = 'postWritePage.realNameNotice'; + static const postWritePage_anonymous = 'postWritePage.anonymous'; + static const postWritePage_adult = 'postWritePage.adult'; + static const postWritePage_politics = 'postWritePage.politics'; + static const postWritePage_contentPlaceholder = 'postWritePage.contentPlaceholder'; + static const postWritePage_conditionSnackBar = 'postWritePage.conditionSnackBar'; + static const postWritePage_noCategory = 'postWritePage.noCategory'; + static const postWritePage_selectCategory = 'postWritePage.selectCategory'; + static const postWritePage = 'postWritePage'; + static const termsAndConditionsPage_termsAndConditions = 'termsAndConditionsPage.termsAndConditions'; + static const termsAndConditionsPage_agree = 'termsAndConditionsPage.agree'; + static const termsAndConditionsPage_agreed = 'termsAndConditionsPage.agreed'; + static const termsAndConditionsPage = 'termsAndConditionsPage'; + static const userProvider_internetError = 'userProvider.internetError'; + static const userProvider = 'userProvider'; + +} diff --git a/lib/utils/cache_function.dart b/lib/utils/cache_function.dart index 869a4ffb..f2747812 100644 --- a/lib/utils/cache_function.dart +++ b/lib/utils/cache_function.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:dio/dio.dart'; import 'package:new_ara_app/providers/user_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -49,14 +50,15 @@ Future updateStateWithCachedOrFetchedApiData({ await callback(recentJson); } // UserProvider를 통해 API로부터 새로운 데이터를 요청합니다. - final dynamic response = await userProvider.getApiRes(apiUrl); + Response? response = await userProvider.getApiRes(apiUrl); + if (response != null) { // 새로운 데이터를 캐시에 저장하고, 콜백 함수를 통해 상태를 업데이트합니다. - await cacheApiData(apiUrl, response); - await callback(response); + await cacheApiData(apiUrl, response.data); + await callback(response.data); } } catch (error) { - debugPrint("updateStateWithCachedOrFetchedApiData error: $error"); + debugPrint("updateStateWithCachedOrFetchedApiData error: $error, apiurl: $apiUrl"); // 에러 발생 시 적절한 에러 처리 로직을 추가합니다. } } diff --git a/lib/utils/global_key.dart b/lib/utils/global_key.dart new file mode 100644 index 00000000..f5e9f9be --- /dev/null +++ b/lib/utils/global_key.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +/// snackBar를 생성할 때, buildContext를 참조할 필요가 없도록 +/// globalKey를 설정합니다. +final GlobalKey snackBarKey = + GlobalKey(); diff --git a/lib/utils/handle_hidden.dart b/lib/utils/handle_hidden.dart index 086e91d0..434aea72 100644 --- a/lib/utils/handle_hidden.dart +++ b/lib/utils/handle_hidden.dart @@ -1,71 +1,109 @@ -/// (2023.02.01) 현재 BE에 있는 모든 게시글 숨김 사유 -/// /// BE가 변경되면 새로 확인해볼 필요 있음. -const Map hiddenReasons = { - "REPORTED_CONTENT": "신고 누적으로 숨김된 게시물입니다.", - "BLOCKED_USER_CONTENT": "차단한 사용자의 게시물입니다.", - "ADULT_CONTENT": "성인/음란성 내용의 게시물입니다.", - "SOCIAL_CONTENT": "정치/사회성 내용의 게시물입니다.", - "ACCESS_DENIED_CONTENT": "접근 권한이 없는 게시물입니다." // BE에서 안전상 만들어둔 것으로 거의 쓰이지 않음 -}; +import 'package:flutter/material.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; -/// (2023.02.01) 현재 BE에 있는 모든 댓글 숨김 사유 -/// BE가 변경되면 새로 확인해볼 필요 있음. -const Map hiddenReasonsComment = { - "REPORTED_CONTENT": "신고 누적으로 숨김된 댓글입니다.", - "BLOCKED_USER_CONTENT": "차단한 사용자의 댓글입니다.", - "DELETED_CONTENT": "삭제된 댓글입니다.", -}; +/// 게시글 정보를 입력받고 그에 상태에 따라 적절한 제목을 리턴하는 함수. + /// UserViewPage, UserPage, PostListShowPage, PostViewPage에서 사용함. + String getTitle( + String? orignialTitle, bool isHidden, List whyHidden) { + // 숨겨진 글이 아닌 경우 + if (isHidden == false) { + return orignialTitle.toString(); + } + // 숨겨졌으나 why_hidden이 지정되지 않은 경우. 혹시 모를 에러 방지를 위해 추가함. + else if (whyHidden.isEmpty) { + return LocaleKeys.postPreview_hiddenPost.tr(); + } -/// (2023.02.01) 현재 BE에 있는 게시글 숨김 사유에 따른 사용자 안내 메시지 -/// 웹과의 UI차이로 인해 앱에 맞게 문구가 변경됨. -const Map hiddenReasonNotices = { - "REPORTED_CONTENT": "", - "BLOCKED_USER_CONTENT": "차단 사용자 설정은 설정페이지에서 하실 수 있습니다.", - "ADULT_CONTENT": "게시글 보기 설정은 설정페이지에서 하실 수 있습니다.", - "SOCIAL_CONTENT": "게시글 보기 설정은 설정페이지에서 하실 수 있습니다.", - "ACCESS_DENIED_CONTENT": "", -}; + // TODO: 새로운 사유가 있을 경우 코드에 반영하기. + late String title; + switch (whyHidden[0]) { + case "REPORTED_CONTENT": + title = LocaleKeys.postPreview_reportedPost.tr(); + break; + case "BLOCKED_USER_CONTENT": + title = LocaleKeys.postPreview_blockedUsersPost.tr(); + break; + case "ADULT_CONTENT": + title = LocaleKeys.postPreview_adultPost.tr(); + break; + case "SOCIAL_CONTENT": + title = LocaleKeys.postPreview_socialPost.tr(); + break; + case "ACCESS_DENIED_CONTENT": + title = LocaleKeys.postPreview_accessDeniedPost.tr(); + break; + // 새로운 whyHidden에 대해서는 숨겨진 게시글로 표기. 이후 앱에서 반영해줘야 함. + default: + debugPrint( + "\n***********************\nANOTHER HIDDEN REASON FOUND: ${whyHidden[0]}\n***********************\n"); + title = LocaleKeys.postPreview_hiddenPost.tr(); + } -/// 게시글 정보를 입력받고 그에 상태에 따라 적절한 제목을 리턴하는 함수. -/// UserViewPage, UserPage, PostListShowPage, PostViewPage에서 사용함. -String getTitle(String? orignialTitle, bool isHidden, List whyHidden) { - // 숨겨진 글이 아닌 경우 - if (isHidden == false) { - return orignialTitle.toString(); - } - // 숨겨졌으나 why_hidden이 지정되지 않은 경우. 혹시 모를 에러 방지를 위해 추가함. - else if (whyHidden.isEmpty) { - return '숨겨진 게시물입니다.'; + return title; } - // TODO: hiddenReasons에 없는 사유가 있을 경우 코드에 반영하기. - return hiddenReasons[whyHidden[0]] ?? "숨겨진 게시물입니다."; -} + /// Article 모델의 why_hidden 값을 입력으로 받아 해당하는 모든 사유에 대한 내용을 담은 리스트를 반환 + /// PostViewPage에서 숨김, 차단된 글에 사용됨. + List getAllHiddenReasons(List whyHidden, Locale locale) { + List reasons = []; + + for (String reason in whyHidden) { + // TODO: hiddenReasons에 없는 사유가 있을 경우 코드에 반영하기. + reasons.add(getTitle("", true, [reason])); + } -/// is_hidden이 True인 댓글에 대해 숨김 사유를 리턴하는 함수 -/// PostViewPage에서 숨겨진 댓글의 내용을 표시할 때 사용됨. -String getHiddenCommentReasons(List whyHidden) { - // TODO: hiddenReasonsComment에 없는 사유가 있을 경우 코드에 반영하기. - return hiddenReasonsComment[whyHidden[0]] ?? "숨겨진 댓글입니다."; -} + return reasons; + } + + /// is_hidden이 True인 댓글에 대해 숨김 사유를 리턴하는 함수 + /// PostViewPage에서 숨겨진 댓글의 내용을 표시할 때 사용됨. + String getHiddenCommentReasons(List whyHidden, Locale locale) { + // 예기치 못한 에러 방지를 위해 추가함 + if (whyHidden.isEmpty) { + return LocaleKeys.postViewPage_hiddenComment.tr(); + } -/// Article 모델의 why_hidden 값을 입력으로 받아 해당하는 모든 사유에 대한 내용을 담은 리스트를 반환 -/// PostViewPage에서 숨김, 차단된 글에 사용됨. -List getAllHiddenReasons(List whyHidden) { - List reasons = []; + late String hiddenReason; + switch (whyHidden[0]) { + case "REPORTED_CONTENT": + hiddenReason = LocaleKeys.postViewPage_reportedComment.tr(); + break; + case "BLOCKED_USER_CONTENT": + hiddenReason = LocaleKeys.postViewPage_blockedUsersComment.tr(); + break; + case "DELETED_CONTENT": + hiddenReason = LocaleKeys.postViewPage_deletedComment.tr(); + break; + default: + debugPrint( + "\n***********************\nANOTHER HIDDEN REASON FOUND: ${whyHidden[0]}\n***********************\n"); + hiddenReason = LocaleKeys.postViewPage_hiddenComment.tr(); + break; + } - for (String reason in whyHidden) { - // TODO: hiddenReasons에 없는 사유가 있을 경우 코드에 반영하기. - reasons.add(hiddenReasons[reason] ?? "숨겨진 게시물입니다."); + return hiddenReason; } - return reasons; -} + /// 사용자에게 숨겨진 글 관련 설정 방법을 알려주는 함수. + String getHiddenInfo(List whyHidden) { + // 숨김 사유가 없을 경우 + if (whyHidden.isEmpty) return ""; + + late String hiddenInfo; + + switch (whyHidden[0]) { + case "BLOCKED_USER_CONTENT": + hiddenInfo = LocaleKeys.postViewPage_blockedUsersContentNotice.tr(); + break; + case "ADULT_CONTENT": + case "SOCIAL_CONTENT": + hiddenInfo = LocaleKeys.postViewPage_adultContentNotice.tr(); + break; + default: + hiddenInfo = ""; + break; + } -/// 사용자에게 숨겨진 글 관련 설정 방법을 알려주는 함수. -String getHiddenInfo(List whyHidden) { - // 숨김 사유가 없을 경우 - if (whyHidden.isEmpty) return ""; - - return hiddenReasonNotices[whyHidden[0]] ?? ""; -} + return hiddenInfo; + } \ No newline at end of file diff --git a/lib/utils/handle_name.dart b/lib/utils/handle_name.dart new file mode 100644 index 00000000..0ae022ac --- /dev/null +++ b/lib/utils/handle_name.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +/// 익명인 경우 '익명', '글쓴이'로 표기되는 닉네임을 로케일에 맞게 변경해준다. +/// 익명(nameType == 2)이면서 nickname이 '익명', '글쓴이'이고 locale이 en인 경우를 제외하면 +/// 원래 nickname을 그대로 리턴한다. +String getName(int? nameType, String nickname, Locale locale) { + String res = nickname; + + // 익명이면서 로케일이 en인 경우 상황에 따라 nickname 변경. + if (nameType == 2 && locale == const Locale('en')) { + if (nickname == '익명') { + res = 'Anonymous'; + } + else if (nickname == '글쓴이') { + res = 'Author'; + } + } + + return res; +} \ No newline at end of file diff --git a/lib/utils/html_info.dart b/lib/utils/html_info.dart index 627db8d3..99495463 100644 --- a/lib/utils/html_info.dart +++ b/lib/utils/html_info.dart @@ -1,5 +1,4 @@ import 'package:sanitize_html/sanitize_html.dart' show sanitizeHtml; -import 'package:flutter/material.dart'; String getContentHtml(String content, {double? width}) { /* sanitize_html 패키지에서는 태그를 제거해버림 @@ -91,7 +90,7 @@ String getContentHtml(String content, {double? width}) { html { -webkit-box-sizing: border-box; box-sizing: border-box - font-family: "Noto Sans KR",sans-serif; + font-family: "Pretendard",sans-serif; } audio,img,video { @@ -129,7 +128,7 @@ String getContentHtml(String content, {double? width}) { } body,button,input,select,textarea { - font-family: "Noto Sans KR",sans-serif + font-family: "Pretendard",sans-serif } code,pre { diff --git a/lib/utils/post_view_utils.dart b/lib/utils/post_view_utils.dart index a7d27647..88529e29 100644 --- a/lib/utils/post_view_utils.dart +++ b/lib/utils/post_view_utils.dart @@ -1,4 +1,4 @@ -/// PostViewPage 내부에서 사용되는 메서드가 많아 별도의 파일로 분류함. +// PostViewPage 내부에서 사용되는 메서드가 많아 별도의 파일로 분류함. import 'dart:io'; import 'package:flutter/material.dart'; @@ -8,7 +8,9 @@ import 'package:path_provider/path_provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/models/article_model.dart'; import 'package:new_ara_app/models/comment_nested_comment_list_action_model.dart'; import 'package:new_ara_app/models/attachment_model.dart'; @@ -16,7 +18,6 @@ import 'package:new_ara_app/models/scrap_create_action_model.dart'; import 'package:new_ara_app/providers/user_provider.dart'; import 'package:new_ara_app/constants/url_info.dart'; import 'package:new_ara_app/constants/colors_info.dart'; -import 'package:new_ara_app/pages/post_view_page.dart'; class ArticleController { ArticleModel model; @@ -32,16 +33,12 @@ class ArticleController { Future posVote() async { if (model.is_mine) return false; int id = model.id; - if (model.my_vote == true) { - var cancelRes = await userProvider.postApiRes( - "articles/$id/vote_cancel/", - ); - if (cancelRes == null || cancelRes.statusCode != 200) return false; - } else { - var postRes = await userProvider.postApiRes( - "articles/$id/vote_positive/", - ); - if (postRes == null || postRes.statusCode != 200) return false; + Response? postRes = await userProvider.postApiRes(model.my_vote == true + ? 'articles/$id/vote_cancel/' + : 'articles/$id/vote_positive/'); + if (postRes == null) { + debugPrint("posVote() failed"); + return false; } return true; } @@ -51,16 +48,12 @@ class ArticleController { Future negVote() async { if (model.is_mine == true) return false; int id = model.id; - if (model.my_vote == false) { - var cancelRes = await userProvider.postApiRes( - "articles/$id/vote_cancel/", - ); - if (cancelRes == null || cancelRes.statusCode != 200) return false; - } else { - var postRes = await userProvider.postApiRes( - "articles/$id/vote_negative/", - ); - if (postRes == null || postRes.statusCode != 200) return false; + Response? postRes = await userProvider.postApiRes(model.my_vote == false + ? 'articles/$id/vote_cancel/' + : 'articles/$id/vote_negative/'); + if (postRes == null) { + debugPrint("negVote() failed"); + return false; } return true; } @@ -92,19 +85,27 @@ class ArticleController { /// 스크랩 관련 API 요청이 성공하면 true, 실패하면 false를 반환. Future scrap() async { if (model.my_scrap == null) { - var postRes = await userProvider.postApiRes( + Response? postRes = await userProvider.postApiRes( "scraps/", - payload: { + data: { "parent_article": model.id, }, ); - if (postRes.statusCode != 201) return false; - model.my_scrap = ScrapCreateActionModel.fromJson(postRes.data); + if (postRes != null) { + model.my_scrap = ScrapCreateActionModel.fromJson(postRes.data); + } else { + debugPrint("scrap() failed"); + return false; + } } else { - var delRes = + Response? delRes = await userProvider.delApiRes("scraps/${model.my_scrap!.id}/"); - if (delRes.statusCode != 204) return false; - model.my_scrap = null; + if (delRes != null) { + model.my_scrap = null; + } else { + debugPrint("scrap() failed"); + return false; + } } return true; } @@ -121,61 +122,32 @@ class ArticleController { /// 전달받은 id에 해당하는 글을 삭제하는 메서드. /// 삭제가 정상적으로 완료되면 true, 아니면 false 반환. Future delete() async { - String apiUrl = "$newAraDefaultUrl/api/articles/${model.id}/"; - try { - await userProvider.createDioWithHeadersForNonget().delete(apiUrl); + String apiUrl = "articles/${model.id}/"; + Response? delRes = await userProvider.delApiRes(apiUrl); + if (delRes != null) { return true; - } on DioException catch (e) { - debugPrint("DioException occurred"); - if (e.response != null) { - debugPrint("${e.response!.data}"); - debugPrint("${e.response!.headers}"); - debugPrint("${e.response!.requestOptions}"); - } - // request의 setting, sending에서 문제 발생 - // requestOption, message를 출력. - else { - debugPrint("${e.requestOptions}"); - debugPrint("${e.message}"); - } - } catch (e) { - debugPrint("error at delete: $e"); + } else { + debugPrint("delete() failed"); + return false; } - return false; } /// post의 작성자에 대한 차단 및 차단 해제 요청을 보내는 함수 /// block이 true이면 차단, false이면 차단 해제 요청을 보냄 /// 성공하면 true, 실패하면 false 리턴. Future handleBlock(bool block) async { - String apiUrl = "$newAraDefaultUrl/api/blocks/"; + String apiUrl = "blocks/"; // 차단 해제하는 경우 apiUrl을 변경 if (!block) apiUrl += "without_id/"; int userID = model.created_by.id; - - try { - await userProvider.createDioWithHeadersForNonget().post( - apiUrl, - data: block ? {'user': userID} : {'blocked': userID} - ); + Response? postRes = await userProvider.postApiRes( + apiUrl, data: block ? {'user': userID} : {'blocked': userID} + ); + if (postRes != null) { return true; - } on DioException catch (e) { - debugPrint("DioException occurred"); - if (e.response != null) { - debugPrint("${e.response!.data}"); - debugPrint("${e.response!.headers}"); - debugPrint("${e.response!.requestOptions}"); - } - // request의 setting, sending에서 문제 발생 - // requestOption, message를 출력. - else { - debugPrint("${e.requestOptions}"); - debugPrint("${e.message}"); - } - } catch (e) { - debugPrint("error on handleBlock: $e"); } + debugPrint("handleBlock() failed"); return false; } @@ -242,7 +214,9 @@ class FileController { /// 다운로드가 성공하면 true, 그렇지 않으면 false 리턴. Future _downloadFile(String uri, String totalPath) async { try { - await userProvider.createDioWithHeadersForNonget().download(uri, totalPath); + await userProvider + .createDioWithHeadersForNonget() + .download(uri, totalPath); } catch (error) { return false; } @@ -263,16 +237,12 @@ class CommentController { Future posVote() async { if (model.is_mine) return false; int id = model.id; - if (model.my_vote == true) { - var cancelRes = await userProvider.postApiRes( - "comments/$id/vote_cancel/", - ); - if (cancelRes == null || cancelRes.statusCode != 200) return false; - } else { - var postRes = await userProvider.postApiRes( - "comments/$id/vote_positive/", - ); - if (postRes == null || postRes.statusCode != 200) return false; + Response? postRes = await userProvider.postApiRes(model.my_vote == true + ? 'comments/$id/vote_cancel/' + : 'comments/$id/vote_positive/'); + if (postRes == null) { + debugPrint("posVote() failed"); + return false; } return true; } @@ -281,16 +251,12 @@ class CommentController { Future negVote() async { if (model.is_mine == true) return false; int id = model.id; - if (model.my_vote == false) { - var cancelRes = await userProvider.postApiRes( - "comments/$id/vote_cancel/", - ); - if (cancelRes == null || cancelRes.statusCode != 200) return false; - } else { - var postRes = await userProvider.postApiRes( - "comments/$id/vote_negative/", - ); - if (postRes == null || postRes.statusCode != 200) return false; + Response? postRes = await userProvider.postApiRes(model.my_vote == false + ? "comments/$id/vote_cancel/" + : "comments/$id/vote_negative/"); + if (postRes == null) { + debugPrint("negVote() failed"); + return false; } return true; } @@ -311,11 +277,11 @@ class CommentController { /// 댓글 식별을 위한 [id], API 통신을 위한 [userProvider]를 전달받음. /// 댓글 삭제 API 요청이 성공하면 true, 그 외에는 false를 반환함. Future delComment(int id, UserProvider userProvider) async { - try { - await userProvider.delApiRes("comments/$id/"); + Response? delRes = await userProvider.delApiRes("comments/$id/"); + if (delRes != null) { return true; - } catch (error) { - debugPrint("DELETE /api/comments/$id failed: $error"); + } else { + debugPrint("delComment() failed"); return false; } } @@ -355,6 +321,15 @@ class _ReportDialogWidgetState extends State { "기타" ]; + List reportContentEng = [ + "Hate Speech", + "Unauthorized Sales", + "Spam", + "Fake Information", + "Defamation", + "Other" + ]; + /// 각각의 신고 내역에 대해 선택되었는지 여부를 나타냄. late List isChosen; @@ -377,19 +352,23 @@ class _ReportDialogWidgetState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SvgPicture.asset( - "assets/icons/information.svg", - width: 45, - height: 45, - color: ColorsInfo.newara, - ), + SvgPicture.asset("assets/icons/information.svg", + width: 45, + height: 45, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, + BlendMode.srcIn, + )), const SizedBox(height: 5), Text( - '${widget.articleID == null ? '댓글' : '게시글'} 신고 사유를 알려주세요.', + widget.articleID == null + ? LocaleKeys.postViewUtils_letUsKnowCommentReportReason.tr() + : LocaleKeys.postViewUtils_letUsKnowPostReportReason.tr(), style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600, ), + textAlign: TextAlign.center, ), const SizedBox(height: 20), _buildReportButton(0), @@ -422,10 +401,10 @@ class _ReportDialogWidgetState extends State { ), width: 60, height: 40, - child: const Center( + child: Center( child: Text( - '취소', - style: TextStyle( + LocaleKeys.postViewUtils_cancel.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, @@ -443,7 +422,11 @@ class _ReportDialogWidgetState extends State { // TODO: postApiRes의 response를 가져와서 신고에 실패한 경우 // e.response가 null이 아닐 경우에는 실패 사유도 출력하도록 변경하기 // 우선은 신고가 실패하면 무조건 '이미 신고한 게시물입니다'로 나오도록 함. (2023.02.16) - showInfoBySnackBar(context, res ? '해당 게시글을 신고했습니다.' : '이미 신고한 게시물입니다.'); + showInfoBySnackBar( + context, + res + ? LocaleKeys.postViewUtils_reportPostSucceed.tr() + : LocaleKeys.postViewUtils_alreadyReported.tr()); }); }, child: Container( @@ -460,10 +443,10 @@ class _ReportDialogWidgetState extends State { ), width: 100, height: 40, - child: const Center( + child: Center( child: Text( - '신고하기', - style: TextStyle( + LocaleKeys.postViewUtils_reportButton.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white, @@ -506,13 +489,12 @@ class _ReportDialogWidgetState extends State { ? {"parent_comment": widget.commentID ?? 0} : {"parent_article": widget.articleID ?? 0}); UserProvider userProvider = context.read(); - try { - await userProvider.postApiRes( - "reports/", - payload: defaultPayload, - ); - } catch (error) { - debugPrint("postReport() failed with error: $error"); + Response? postRes = await userProvider.postApiRes( + "reports/", + data: defaultPayload, + ); + if (postRes == null) { + debugPrint("postReport() failed with error"); return false; } @@ -537,7 +519,9 @@ class _ReportDialogWidgetState extends State { height: 40, child: Center( child: Text( - reportContentKor[idx], + context.locale == const Locale('ko') + ? reportContentKor[idx] + : reportContentEng[idx], style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, diff --git a/lib/utils/slide_routing.dart b/lib/utils/slide_routing.dart index a3bdbef8..22117e96 100644 --- a/lib/utils/slide_routing.dart +++ b/lib/utils/slide_routing.dart @@ -1,25 +1,27 @@ -import 'package:flutter/material.dart'; +import 'dart:io'; import 'package:flutter/cupertino.dart'; Route slideRoute(Widget dst) { // iOS, Android에서 모두 back swipe 적용 // TODO: Android에서 back swipe 적용할지 여부 논의 (2023.01.24) - return CupertinoPageRoute( - builder: (context) => dst, - ); - // return PageRouteBuilder( - // pageBuilder: (context, animation, secondaryAnimation) => dst, - // transitionsBuilder: ((context, animation, secondaryAnimation, child) { - // var begin = const Offset(1, 0); - // var end = const Offset(0, 0); - // var curve = Curves.ease; + if (Platform.isIOS) { + return CupertinoPageRoute( + builder: (context) => dst, + ); + } + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => dst, + transitionsBuilder: ((context, animation, secondaryAnimation, child) { + var begin = const Offset(1, 0); + var end = const Offset(0, 0); + var curve = Curves.ease; - // var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); - // var offsetAnimation = animation.drive(tween); - // return SlideTransition( - // position: offsetAnimation, - // child: child, - // ); - // }), - // ); + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var offsetAnimation = animation.drive(tween); + return SlideTransition( + position: offsetAnimation, + child: child, + ); + }), + ); } diff --git a/lib/utils/time_utils.dart b/lib/utils/time_utils.dart index 824552ca..1cd1509c 100644 --- a/lib/utils/time_utils.dart +++ b/lib/utils/time_utils.dart @@ -1,31 +1,41 @@ +import 'dart:core'; +import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; /// parameter로 전달받은 rawTime을 /// {}년 {}월 {}일 {}:{} 형식으로 변환하여 리턴. -String specificTime(String rawTime) { +String specificTime(String rawTime, Locale locale) { DateTime date = DateTime.parse(rawTime).toLocal(); - String time = '${DateFormat('yyyy').format(date)}년 ${DateFormat('MM').format(date)}월 ${DateFormat('dd').format(date)}일 ${DateFormat('HH').format(date)}:${DateFormat('mm').format(date)}'; + String time = locale == const Locale('ko') + ? '${DateFormat('yyyy').format(date)}년 ${DateFormat('MM').format(date)}월 ${DateFormat('dd').format(date)}일 ${DateFormat('HH').format(date)}:${DateFormat('mm').format(date)}' + : '${DateFormat('yyyy').format(date)}. ${DateFormat('MM').format(date)}. ${DateFormat('dd').format(date)}. ${DateFormat('HH').format(date)}:${DateFormat('mm').format(date)}'; return time; } -String getTime(String rawTime) { +String getTime(String rawTime, Locale locale) { DateTime now = DateTime.now(); DateTime date = DateTime.parse(rawTime).toLocal(); var difference = now.difference(date); - String time = "미정"; + String time = locale == const Locale('ko') ? "미정" : "Not specified"; if (difference.inMinutes < 1) { - time = "${difference.inSeconds}초 전"; + time = + "${difference.inSeconds}${locale == const Locale('ko') ? "초 전" : " seconds ago"}"; } else if (difference.inHours < 1) { - time = '${difference.inMinutes}분 전'; - } else if (date.day == now.day) { + time = + '${difference.inMinutes}${locale == const Locale('ko') ? "분 전" : " minutes ago"}'; + } else if (date.year == now.year && + date.month == now.month && + date.day == now.day) { time = '${DateFormat('HH').format(date)}:${DateFormat('mm').format(date)}'; } else if (date.year == now.year) { - time = '${DateFormat('MM').format(date)}/${DateFormat('dd').format(date)}'; + time = locale == const Locale('ko') + ? DateFormat('MM/dd').format(date) + : DateFormat('MMM d', 'en_US').format(date); } else { - time = - '${DateFormat('yyyy').format(date)}/${DateFormat('MM').format(date)}/${DateFormat('dd').format(date)}'; + time = locale == const Locale('ko') + ? DateFormat('yyyy/MM/dd').format(date) + : DateFormat('MMM d, yyyy', 'en_US').format(date); } - return time; } diff --git a/lib/utils/with_school.dart b/lib/utils/with_school.dart new file mode 100644 index 00000000..3616856c --- /dev/null +++ b/lib/utils/with_school.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; + +import 'package:new_ara_app/translations/locale_keys.g.dart'; +import 'package:new_ara_app/constants/colors_info.dart'; + +/// ArticleModel의 communication_article_status가 가질 수 있는 값 0, 1, 2에 각각 대응되도록 enum 선언. +/// beforeUpVoteThreshold: 0, '달성 전' +/// beforeSchoolconfirm: 1, '답변 대기 중' +/// answerDone: 2, '답변 완료' +/// 가독성을 위해 추가함. +enum WithSchoolStatus { beforeUpVoteThreshold, beforeSchoolConfirm, answerDone } + +/// magicNum: communication_article_status 값 (0, 1, 2 중 하나) +/// magincNum을 입력받고 '달성 전', '답변 대기 중', '답변 완료' 중 하나를 알맞게 리턴함. +String defineCommunicationStatus(int? magicNum) { + late String status; + if (magicNum == WithSchoolStatus.beforeUpVoteThreshold.index) { + status = LocaleKeys.postPreview_beforeUpVoteThreshold.tr(); + } else if (magicNum == WithSchoolStatus.beforeSchoolConfirm.index) { + status = LocaleKeys.postPreview_beforeSchoolConfirm.tr(); + } else if (magicNum == WithSchoolStatus.answerDone.index) { + status = LocaleKeys.postPreview_answerDone.tr(); + } else { + // 위 경우에 해당하지 않는 경우에는 우선 '달성 전'으로 표기 + debugPrint("with-school status: undefined status $magicNum"); + status = LocaleKeys.postPreview_beforeUpVoteThreshold.tr(); + } + + return status; +} + +/// communicationArticleStatus를 전달받고 '달성 전' 상태인지 여부를 리턴 +/// '달성 전' 상태이면 true, 아니면 false. +bool isBeforeUpVoteThreshold(int? communicationArticleStatus) { + return communicationArticleStatus == + WithSchoolStatus.beforeUpVoteThreshold.index; +} + +/// communicationArticleStatus를 전달받고 '답변 대기 중' 상태인지 여부를 리턴 +/// '답변 대기 중' 상태이면 true, 아니면 false. +bool isBeforeSchoolConfirm(int? communicationArticleStatus) { + return communicationArticleStatus == + WithSchoolStatus.beforeSchoolConfirm.index; +} + +/// communicationArticleStatus를 전달받고 '답변 완료' 상태인지 여부를 리턴. +/// '답변 완료' 상태이면 true, 아니면 false 리턴. +bool isAnswerDone(int? communicationArticleStatus) { + return communicationArticleStatus == WithSchoolStatus.answerDone.index; +} + +/// PostViewPage에 communicationArticleStatus에 맞게 텍스트가 추가된 위젯을 리턴 +/// PostViewPage에서만 사용함. +Widget buildWithSchoolStatusBox(int? communicationArticleStatus) { + return Row( + children: [ + const SizedBox(width: 10), + Container( + decoration: BoxDecoration( + color: isAnswerDone(communicationArticleStatus) + ? ColorsInfo.newara + : Colors.white, + border: Border.all( + color: isBeforeUpVoteThreshold(communicationArticleStatus) + ? const Color(0xFFBBBBBB) + : ColorsInfo.newara, + width: 1.0), + borderRadius: const BorderRadius.all(Radius.circular(4))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 1.0), + child: Text( + defineCommunicationStatus(communicationArticleStatus), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: isBeforeUpVoteThreshold(communicationArticleStatus) + ? const Color(0xFFBBBBBB) + : (isBeforeSchoolConfirm(communicationArticleStatus) + ? ColorsInfo.newara + : Colors.white), + ), + )), + ), + ], + ); +} diff --git a/lib/widgets/dialogs.dart b/lib/widgets/dialogs.dart index 3f633c79..17d503b2 100644 --- a/lib/widgets/dialogs.dart +++ b/lib/widgets/dialogs.dart @@ -1,8 +1,10 @@ -/// PostViewPage의 글/댓글 신고, 댓글 수정에 사용되는 Dialog 위젯 파일. +// PostViewPage의 글/댓글 신고, 댓글 수정에 사용되는 Dialog 위젯 파일. import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/providers/user_provider.dart'; import 'package:new_ara_app/constants/colors_info.dart'; @@ -17,6 +19,7 @@ import 'package:new_ara_app/constants/url_info.dart'; */ +// TODO: 현재 post_view_utils.dart와 겹침. 리팩토링 필요 /// 신고 기능이 글, 댓글 모두에게 필요하여 만든 위젯. class ReportDialog extends StatefulWidget { /// 글에 대한 신고일 경우 null이 아님. @@ -73,12 +76,13 @@ class _ReportDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SvgPicture.asset( - "assets/icons/information.svg", - width: 45, - height: 45, - color: ColorsInfo.newara, - ), + SvgPicture.asset("assets/icons/information.svg", + width: 45, + height: 45, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, + BlendMode.srcIn, + )), const SizedBox(height: 5), Text( '${widget.articleID == null ? '댓글' : '게시글'} 신고 사유를 알려주세요.', @@ -198,13 +202,10 @@ class _ReportDialogState extends State { ? {"parent_comment": widget.commentID ?? 0} : {"parent_article": widget.articleID ?? 0}); UserProvider userProvider = context.read(); - try { - await userProvider.postApiRes( - "reports/", - payload: defaultPayload, - ); - } catch (error) { - debugPrint("postReport() failed with error: $error"); + Response? postRes = + await userProvider.postApiRes('reports/', data: defaultPayload); + if (postRes == null) { + debugPrint("postReport() failed with error"); return false; } @@ -276,14 +277,21 @@ class DeleteDialog extends StatelessWidget { 'assets/icons/information.svg', width: 55, height: 55, - color: ColorsInfo.newara, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, + BlendMode.srcIn, + ), ), - const Text( - '정말로 삭제하시겠습니까?', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w700, - color: Colors.black, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Text( + LocaleKeys.dialogs_deleteConfirm.tr(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: Colors.black, + ), ), ), const SizedBox(height: 20), @@ -307,10 +315,10 @@ class DeleteDialog extends StatelessWidget { ), width: 60, height: 40, - child: const Center( + child: Center( child: Text( - '취소', - style: TextStyle( + LocaleKeys.dialogs_cancel.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, @@ -330,10 +338,10 @@ class DeleteDialog extends StatelessWidget { ), width: 60, height: 40, - child: const Center( + child: Center( child: Text( - '확인', - style: TextStyle( + LocaleKeys.dialogs_confirm.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white, @@ -379,13 +387,12 @@ class _BlockedUserDialogState extends State { /// 성공 시에 BlockModel의 리스트를 반환함. 실패할 시 empty list 반환. Future> fetchBlockedUsers() async { UserProvider userProvider = context.read(); - String apiUrl = "/api/blocks/"; + String apiUrl = "blocks/"; List resList = []; try { - var response = await userProvider - .createDioWithHeadersForGet() - .get("$newAraDefaultUrl$apiUrl"); - List jsonUserList = response.data['results']; + var response = await userProvider.getApiRes(apiUrl); + final Map? jsonList = await response?.data; + List jsonUserList = jsonList?['results']; for (Map json in jsonUserList) { try { resList.add(BlockModel.fromJson(json)); @@ -425,10 +432,10 @@ class _BlockedUserDialogState extends State { height: 55, margin: const EdgeInsets.only( left: 15, right: 15, top: 5, bottom: 5), - child: const Center( + child: Center( child: Text( - "차단한 유저가 없습니다", - style: TextStyle( + LocaleKeys.dialogs_noBlockedUsers.tr(), + style: const TextStyle( color: Colors.black, fontSize: 15, fontWeight: FontWeight.w500, @@ -505,7 +512,7 @@ class _BlockedUserDialogState extends State { Expanded( child: Text( blockedUser.user.profile.nickname ?? - "닉네임이 없음", + LocaleKeys.dialogs_noNickname.tr(), overflow: TextOverflow.ellipsis, maxLines: 1, style: const TextStyle( @@ -551,29 +558,14 @@ class _BlockedUserDialogState extends State { /// 성공하면 true. 아니면 false 반환. Future unblockUser(int userID) async { UserProvider userProvider = context.read(); - String apiUrl = "/api/blocks/$userID/"; - try { - await userProvider - .createDioWithHeadersForNonget() - .delete("$newAraDefaultUrl$apiUrl"); + String apiUrl = "blocks/$userID/"; + Response? delRes = await userProvider.delApiRes(apiUrl); + if (delRes != null) { return true; - } on DioException catch (e) { - debugPrint("DioException occurred"); - if (e.response != null) { - debugPrint("${e.response!.data}"); - debugPrint("${e.response!.headers}"); - debugPrint("${e.response!.requestOptions}"); - } - // request의 setting, sending에서 문제 발생 - // requestOption, message를 출력. - else { - debugPrint("${e.requestOptions}"); - debugPrint("${e.message}"); - } - } catch (e) { - debugPrint("unblock error: $e"); + } else { + debugPrint("unblockUser() failed"); + return false; } - return false; } } @@ -611,14 +603,21 @@ class BlockConfirmDialog extends StatelessWidget { 'assets/icons/information.svg', width: 55, height: 55, - color: ColorsInfo.newara, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, + BlendMode.srcIn, + ), ), - const Text( - '정말로 차단하시겠습니까?', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w700, - color: Colors.black, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Text( + LocaleKeys.dialogs_blockConfirm.tr(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: Colors.black, + ), ), ), const SizedBox(height: 20), @@ -642,10 +641,10 @@ class BlockConfirmDialog extends StatelessWidget { ), width: 60, height: 40, - child: const Center( + child: Center( child: Text( - '취소', - style: TextStyle( + LocaleKeys.dialogs_cancel.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, @@ -665,10 +664,10 @@ class BlockConfirmDialog extends StatelessWidget { ), width: 60, height: 40, - child: const Center( + child: Center( child: Text( - '확인', - style: TextStyle( + LocaleKeys.dialogs_confirm.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white, @@ -719,17 +718,19 @@ class SignoutConfirmDialog extends StatelessWidget { 'assets/icons/information.svg', width: 55, height: 55, - color: ColorsInfo.newara, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, + BlendMode.srcIn, + ), ), - const Text( - '정말로 로그아웃 하시겠습니까?', - style: TextStyle( + Text( + LocaleKeys.dialogs_logoutConfirm.tr(), + style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: Colors.black, ), ), - const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -751,10 +752,10 @@ class SignoutConfirmDialog extends StatelessWidget { ), width: 60, height: 40, - child: const Center( + child: Center( child: Text( - '취소', - style: TextStyle( + LocaleKeys.dialogs_cancel.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, @@ -774,10 +775,10 @@ class SignoutConfirmDialog extends StatelessWidget { ), width: 60, height: 40, - child: const Center( + child: Center( child: Text( - '확인', - style: TextStyle( + LocaleKeys.dialogs_confirm.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white, @@ -818,7 +819,7 @@ class UnregisterConfirmDialog extends StatelessWidget { decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(15)), ), - width: 350, + width: 360, height: 200, child: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -828,21 +829,29 @@ class UnregisterConfirmDialog extends StatelessWidget { 'assets/icons/information.svg', width: 55, height: 55, - color: ColorsInfo.newara, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, + BlendMode.srcIn, + ), ), - const Text( - '정말로 회원탈퇴 하시겠습니까?', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w700, - color: Colors.black, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 35), + child: Text( + LocaleKeys.dialogs_withdrawalConfirm.tr(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: Colors.black, + ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 50), - child: const Text( - '회원탈퇴하시면 지금 쓰시는 이메일로는 재가입이 불가능합니다', - style: TextStyle( + child: Text( + LocaleKeys.dialogs_withdrawalEmailInfo.tr(), + textAlign: TextAlign.center, + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: Color(0xFFBBBBBB), @@ -870,10 +879,10 @@ class UnregisterConfirmDialog extends StatelessWidget { ), width: 60, height: 40, - child: const Center( + child: Center( child: Text( - '취소', - style: TextStyle( + LocaleKeys.dialogs_cancel.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, @@ -882,7 +891,7 @@ class UnregisterConfirmDialog extends StatelessWidget { ), ), ), - const SizedBox(width: 10), + const SizedBox(width: 5), InkWell( // 인자로 전달받은 onTap 사용. onTap: onTap, @@ -893,10 +902,10 @@ class UnregisterConfirmDialog extends StatelessWidget { ), width: 60, height: 40, - child: const Center( + child: Center( child: Text( - '확인', - style: TextStyle( + LocaleKeys.dialogs_confirm.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white, @@ -906,10 +915,169 @@ class UnregisterConfirmDialog extends StatelessWidget { ), ), ], - ) + ), ], ), ), ); } -} \ No newline at end of file +} + +class ForAndroidTesterDialog extends StatelessWidget { + final UserProvider userProvider; + final _scrollController = ScrollController(); + + /// PostViewPage의 context. + final BuildContext targetContext; + + /// '확인' 버튼을 눌렀을 때 적용되는 onTap 메서드 + final void Function()? onTap; + + ForAndroidTesterDialog({ + super.key, + required this.userProvider, + required this.targetContext, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(15)), + ), + child: SizedBox( + width: 350, + height: 500, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/icons/information.svg', + width: 55, + height: 55, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, + BlendMode.srcIn, + ), + ), + const Text( + 'Dear Google Play Review Team,', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB( + 30, + 10, + 20, + 10, + ), + child: Scrollbar( + thumbVisibility: true, + controller: _scrollController, + child: Padding( + padding: const EdgeInsets.only(right: 10), + child: ListView( + controller: _scrollController, + children: const [ + Text( + '''I am writing to seek assistance regarding an issue we have encountered following the production review of our app on the Google Play Store. + +During our production review process, we observed an unusual volume of comments within our app, which appear to be related to the review process itself. These abnormal comments were made using an account provided exclusively to Google Play Store reviewers. This situation has caused significant inconvenience, especially for students who rely on our app for communication and educational purposes. + +While we understand the importance of thorough app reviews and user feedback, we suspect that a misunderstanding or a technical issue has led to the excessive commenting. This not only affects the app's performance but also its acceptance among our target audience. + +To resolve this issue, we would greatly appreciate it if you could provide guidelines on why comment posting is necessary during the review process and what features we should demonstrate. Our goal is to ensure a positive and constructive environment for our users while adhering to Google Play's policies and standards. + +We are thankful for your understanding and support on this matter. Please let us know if there are specific procedures we should follow or if additional information is required from our side. + +Thank you for your attention and support on this issue. We look forward to your prompt response. + +Sincerely, + + +안녕하십니까? 구글 플레이 스토어의 프로덕션 심사 후 저희가 마주한 문제에 대해 도움을 요청하기 위해 이 메시지를 작성합니다. + +저희 프로덕션 심사 과정 중, 앱 내에서 심사 과정과 관련된 것으로 보이는 비정상적인 댓글 양을 관찰했습니다. 이 비정상적인 댓글은 구글 플레이 스토어 심사관에게만 제공한 계정을 통해 작성되었습니다. 이는 특히 저희 앱을 소통과 교육 목적으로 의존하는 학생들의 사용자 경험에 큰 불편을 초래하고 있습니다. + +철저한 앱 리뷰와 사용자 피드백의 중요성을 이해하고 있지만, 과도한 댓글이 발생한 배경에는 오해나 기술적 문제가 있을 것으로 생각합니다. 이는 앱의 성능뿐만 아니라 저희 타겟 오디언스 사이에서의 수용성에도 영향을 미치고 있습니다. + +이 문제를 해결하기 위해, 심사 과정 중 앱 내에서 댓글 작성이 필요한 이유와 저희가 어떤 기능을 보여줘야 하는지에 대한 가이드라인을 제시해주시면 감사하겠습니다. 저희의 목표는 구글 플레이의 정책과 기준을 준수하면서 사용자에게 긍정적이고 건설적인 환경을 보장하는 것입니다. + +이 문제에 대한 이해와 지원에 감사드립니다. 저희가 따라야 할 특정한 절차가 있거나 저희 측에서 추가 정보가 필요한 경우 알려주시기 바랍니다. + +이 문제에 주목해주시고 지원해주셔서 감사합니다. 신속한 답변과 지원을 기대합니다. + +감사합니다, + +Google Playストアでのアプリのプロダクションレビュー後に遭遇した問題について支援を求めています。 + +プロダクションレビュープロセス中に、レビュープロセス自体に関連していると思われるアプリ内での異常なコメントの量を観察しました。これらの異常なコメントは、Google Playストアのレビュアーにのみ提供されたアカウントを使用して作成されました。この状況は、通信および教育目的で私たちのアプリに依存している学生を含むユーザーにとって、大きな不便を引き起こしています。 + +徹底的なアプリレビューとユーザーフィードバックの重要性を理解していますが、過剰なコメントにつながった誤解または技術的な問題があると疑っています。これは、アプリのパフォーマンスだけでなく、ターゲットオーディエンスの間での受け入れにも影響を与えています。 + +この問題を解決するために、レビュープロセス中にコメント投稿が必要な理由と、私たちが示すべき機能についてのガイドラインを提供していただければ幸いです。私たちの目標は、Google Playのポリシーと基準を遵守しながら、ユーザーにとって肯定的で建設的な環境を確保することです。 + +この件に関するご理解とサポートに感謝します。特定の手順を踏むべきか、私たちの側から追加情報が必要かどうか教えてください。 + +この問題に対するご注意とサポートに感謝します。迅速なご返答をお待ちしています。 + +敬具、 + ''', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: Color(0xFFBBBBBB), + ), + ), + ], + ), + ), + ), + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + // 인자로 전달받은 onTap 사용. + onTap: onTap, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(20)), + color: ColorsInfo.newara, + ), + width: 80, + height: 40, + child: const Center( + child: Text( + 'Confirm', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + const SizedBox( + height: 20, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart index f10808c6..71240fd3 100644 --- a/lib/widgets/loading_indicator.dart +++ b/lib/widgets/loading_indicator.dart @@ -5,7 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:new_ara_app/constants/colors_info.dart'; class LoadingIndicator extends StatelessWidget { - const LoadingIndicator({Key? key}) : super(key: key); + const LoadingIndicator({super.key}); @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/widgets/pop_up_menu_buttons.dart b/lib/widgets/pop_up_menu_buttons.dart index 485e66bd..8f68c189 100644 --- a/lib/widgets/pop_up_menu_buttons.dart +++ b/lib/widgets/pop_up_menu_buttons.dart @@ -1,8 +1,10 @@ -/// PostViewPage에 쓰이는 PopupMenuButton 위젯 일부를 클래스화한 파일 -/// 첨부파일, 타인의 댓글에 사용되는 PopupMenuButton을 클래스화함. +// PostViewPage에 쓰이는 PopupMenuButton 위젯 일부를 클래스화한 파일 +// 첨부파일, 타인의 댓글에 사용되는 PopupMenuButton을 클래스화함. import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/models/attachment_model.dart'; import 'package:new_ara_app/providers/user_provider.dart'; @@ -11,6 +13,47 @@ import 'package:new_ara_app/utils/post_view_utils.dart'; import 'package:new_ara_app/constants/colors_info.dart'; import 'package:new_ara_app/widgets/snackbar_noti.dart'; +class WithSchoolPopupMenuButton extends StatelessWidget { + const WithSchoolPopupMenuButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + shadowColor: const Color.fromRGBO(0, 0, 0, 0.2), + splashRadius: 5, + shape: const RoundedRectangleBorder( + side: BorderSide(color: Color.fromRGBO(217, 217, 217, 1), width: 0.5), + borderRadius: BorderRadius.all(Radius.circular(12.0))), + padding: const EdgeInsets.all(2.0), + offset: Offset(0, 45), + child: SvgPicture.asset( + 'assets/icons/information.svg', + colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), + width: 32, + height: 32, + ), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: 'info', + child: Text( + LocaleKeys.popUpMenuButtons_withSchoolInfoText.tr(), + style: const TextStyle( + color: ColorsInfo.newara, + fontWeight: FontWeight.w400, + fontSize: 15, + ), + ), + ), + ], + onSelected: (String result) { + switch (result) {} + }, + ); + } +} + /// PostViewPage에서 첨부파일 표시에 쓰이는 PopupMenuButton class AttachPopupMenuButton extends StatelessWidget { /// 첨부파일의 개수 @@ -57,7 +100,9 @@ class AttachPopupMenuButton extends StatelessWidget { height: 32, ), Text( - res ? "파일 다운로드에 성공했습니다" : "파일 다운로드에 실패했습니다.", + res + ? LocaleKeys.popUpMenuButtons_downloadSucceed.tr() + : LocaleKeys.popUpMenuButtons_downloadFailed.tr(), style: const TextStyle( color: Colors.black, fontWeight: FontWeight.w400, @@ -70,7 +115,7 @@ class AttachPopupMenuButton extends StatelessWidget { }); }, child: Text( - '첨부파일 모아보기 $fileNum', + '${LocaleKeys.popUpMenuButtons_attachments.tr()} $fileNum', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -156,7 +201,7 @@ class AttachPopupMenuButton extends StatelessWidget { debugPrint(ext); return SvgPicture.asset( assetPath, - color: Colors.black, + colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), width: 30, height: 30, ); @@ -184,7 +229,7 @@ class OthersPopupMenuButton extends StatelessWidget { padding: const EdgeInsets.all(2.0), child: SvgPicture.asset( 'assets/icons/menu_2.svg', - color: Colors.grey, + colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), width: 50, height: 20, ), @@ -215,16 +260,15 @@ class OthersPopupMenuButton extends StatelessWidget { value: 'Report', child: Row( children: [ - SvgPicture.asset( - 'assets/icons/warning.svg', - width: 20, - height: 20, - color: ColorsInfo.newara, - ), + SvgPicture.asset('assets/icons/warning.svg', + width: 20, + height: 20, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, BlendMode.srcIn)), const SizedBox(width: 10), - const Text( - '신고', - style: TextStyle( + Text( + LocaleKeys.popUpMenuButtons_report.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: ColorsInfo.newara, @@ -294,12 +338,15 @@ class MyPopupMenuButton extends StatelessWidget { 'assets/icons/modify.svg', width: 25, height: 25, - color: const Color.fromRGBO(51, 51, 51, 1), + colorFilter: const ColorFilter.mode( + Color.fromRGBO(51, 51, 51, 1), + BlendMode.srcIn, + ), ), const SizedBox(width: 10), - const Text( - '수정', - style: TextStyle( + Text( + LocaleKeys.popUpMenuButtons_edit.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: Color.fromRGBO(51, 51, 51, 1)), @@ -311,16 +358,15 @@ class MyPopupMenuButton extends StatelessWidget { value: 'Delete', child: Row( children: [ - SvgPicture.asset( - 'assets/icons/delete.svg', - width: 25, - height: 25, - color: ColorsInfo.newara, - ), + SvgPicture.asset('assets/icons/delete.svg', + width: 25, + height: 25, + colorFilter: const ColorFilter.mode( + ColorsInfo.newara, BlendMode.srcIn)), const SizedBox(width: 10), - const Text( - '삭제', - style: TextStyle( + Text( + LocaleKeys.popUpMenuButtons_delete.tr(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: ColorsInfo.newara, @@ -333,7 +379,7 @@ class MyPopupMenuButton extends StatelessWidget { onSelected: onSelected, child: SvgPicture.asset( 'assets/icons/menu_2.svg', - color: Colors.grey, + colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), width: 50, height: 20, ), diff --git a/lib/widgets/post_preview.dart b/lib/widgets/post_preview.dart index 1d19df67..0a3d4963 100644 --- a/lib/widgets/post_preview.dart +++ b/lib/widgets/post_preview.dart @@ -1,12 +1,17 @@ import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; import 'package:new_ara_app/models/article_list_action_model.dart'; import 'package:new_ara_app/utils/time_utils.dart'; -import 'package:new_ara_app/utils/handle_hidden.dart'; import 'package:new_ara_app/providers/blocked_provider.dart'; +import 'package:new_ara_app/utils/handle_hidden.dart'; import 'package:provider/provider.dart'; +import 'package:new_ara_app/utils/handle_name.dart'; +import 'package:new_ara_app/constants/colors_info.dart'; +import 'package:new_ara_app/utils/with_school.dart'; class PostPreview extends StatefulWidget { final ArticleListActionModel model; @@ -21,7 +26,7 @@ class _PostPreviewState extends State { Widget build(BuildContext context) { BlockedProvider blockedProvider = context.watch(); - String time = getTime(widget.model.created_at.toString()); + String time = getTime(widget.model.created_at.toString(), context.locale); return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, @@ -33,7 +38,9 @@ class _PostPreviewState extends State { children: [ if (widget.model.parent_topic != null) Text( - "[${widget.model.parent_topic!.ko_name}] ", + context.locale == const Locale('ko') + ? "[${widget.model.parent_topic!.ko_name}] " + : "[${widget.model.parent_topic!.en_name}] ", style: const TextStyle( color: Color(0xFFED3A3A), fontWeight: FontWeight.w500, @@ -47,7 +54,7 @@ class _PostPreviewState extends State { (isAnonymousIOS(widget.model) && blockedProvider.blockedAnonymousPostIDs .contains(widget.model.created_by.id)) - ? "차단한 사용자의 게시물입니다." + ? LocaleKeys.postPreview_blockedUsersPost.tr() : getTitle(widget.model.title, widget.model.is_hidden, widget.model.why_hidden), style: TextStyle( @@ -105,9 +112,29 @@ class _PostPreviewState extends State { Expanded( child: Row( children: [ + if (widget.model.parent_board.slug == 'with-school') + Row( + children: [ + Text( + defineCommunicationStatus( + widget.model.communication_article_status), + style: TextStyle( + fontSize: 12, + color: widget.model.communication_article_status == + WithSchoolStatus.beforeUpVoteThreshold.index + ? const Color(0xFFB1B1B1) + : ColorsInfo.newara, + ), + ), + const SizedBox(width: 8) + ], + ), Flexible( child: Text( - widget.model.created_by.profile.nickname.toString(), + getName( + widget.model.name_type, + widget.model.created_by.profile.nickname.toString(), + context.locale), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( diff --git a/lib/widgets/snackbar_noti.dart b/lib/widgets/snackbar_noti.dart index 93454636..5f9cc874 100644 --- a/lib/widgets/snackbar_noti.dart +++ b/lib/widgets/snackbar_noti.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:new_ara_app/utils/global_key.dart'; const Duration _araSnackBarDisplayDuration = Duration(milliseconds: 2500); @@ -80,6 +81,59 @@ SnackBar buildAraSnackBar(context, ); } +/// 위 buildAraSnackBar와 동일하나, +/// context를 parameter로 받지 않고 +/// global key인 snackBarKey를 참조하도록 함. +SnackBar buildAraSnackBarFromGlobalKey({ + Key? key, + required Widget content, + Color? backgroundColor = Colors.white, // ara specific + double? elevation, + EdgeInsetsGeometry? margin = + const EdgeInsets.only(left: 16, right: 16, bottom: 20), // ara specific + EdgeInsetsGeometry? padding = + const EdgeInsets.only(top: 15, bottom: 15, left: 12), // ara specific + double? width, + ShapeBorder? shape = const RoundedRectangleBorder( + side: BorderSide(color: Color(0xFFF0F0F0), width: 0.5), // ara specific + borderRadius: BorderRadius.all(Radius.circular(16))), + SnackBarBehavior? behavior = SnackBarBehavior.floating, // ara specific + SnackBarAction? action, + double? actionOverflowThreshold, + bool? showCloseIcon, + Color? closeIconColor, + Duration duration = _araSnackBarDisplayDuration, + Animation? animation, + VoidCallback? onVisible, + DismissDirection dismissDirection = DismissDirection.down, + Clip clipBehavior = Clip.hardEdge, +}) { + // snackBarKey의 현재 context에서 width size 가져오기. + final screenWidth = MediaQuery.of(snackBarKey.currentContext!).size.width; + + return SnackBar( + key: key, + content: Center( + child: SizedBox(width: screenWidth - 34, child: content), + ), + backgroundColor: backgroundColor, + elevation: elevation, + margin: margin, + padding: padding, + width: width, + shape: shape, + behavior: behavior, + action: action, + actionOverflowThreshold: actionOverflowThreshold, + showCloseIcon: showCloseIcon, + duration: const Duration(minutes: 10), + animation: animation, + onVisible: onVisible, + dismissDirection: dismissDirection, + clipBehavior: clipBehavior, + ); +} + /// 기존에 존재하는 스낵바로 인해 새로 호출한 스낵바가 queued되었다가 나중에 표시되는 현상을 방지하기 위해 사용. /// 새로운 스낵바 표시 전에 기존의 스낵바를 모두 숨김처리함. void hideOldsAndShowAraSnackBar(context, SnackBar araSnackBar) { @@ -117,3 +171,36 @@ void showInfoBySnackBar(BuildContext context, String infoText) { ], ))); } + +/// internet error가 발생할 때 errorText를 전달하면 반복적으로 생성이 가능하도록 함수화함. +/// 추후 information.svg 외 wifi 오류를 나타낼 수 있는 아이콘을 추가할 예정. +void showInternetErrorBySnackBar(String errorText) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // 이전에 존재하던 스낵바 제거 + //snackBarKey.currentState?.hideCurrentSnackBar(); + snackBarKey.currentState?.showSnackBar(buildAraSnackBarFromGlobalKey( + content: Row( + children: [ + SvgPicture.asset( + 'assets/icons/information.svg', + colorFilter: const ColorFilter.mode(Colors.red, BlendMode.srcIn), + width: 32, + height: 32, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + errorText, + // 오버플로우 나면 다음줄로 넘어가도록 하기 위해 + overflow: TextOverflow.visible, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 15, + ), + ), + ), + ], + ))); + }); +} diff --git a/lib/widgets/text_and_switch.dart b/lib/widgets/text_and_switch.dart index c4468fea..e7768229 100644 --- a/lib/widgets/text_and_switch.dart +++ b/lib/widgets/text_and_switch.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:new_ara_app/constants/colors_info.dart'; +import 'package:new_ara_app/translations/locale_keys.g.dart'; List switchLights = [ true, @@ -15,23 +16,23 @@ List switchLights = [ List> switchItems = [ [ const SizedBox(height: 10), - TextAndSwitch('setting_page.adult'.tr(), 0), + TextAndSwitch(LocaleKeys.settingPage_adult.tr(), 0), const SizedBox(height: 16), - TextAndSwitch('setting_page.politics'.tr(), 1), + TextAndSwitch(LocaleKeys.settingPage_politics.tr(), 1), const SizedBox(height: 10), ], [ const SizedBox(height: 10), - TextAndSwitch('setting_page.myreply'.tr(), 2), + TextAndSwitch(LocaleKeys.settingPage_myReplies.tr(), 2), const SizedBox(height: 16), - TextAndSwitch('setting_page.reply'.tr(), 3), + TextAndSwitch(LocaleKeys.settingPage_replies.tr(), 3), const SizedBox(height: 10), ], [ const SizedBox(height: 10), - TextAndSwitch('setting_page.hot_noti'.tr(), 4), + TextAndSwitch(LocaleKeys.settingPage_hotPosts.tr(), 4), const SizedBox(height: 16), - TextAndSwitch('setting_page.hot_posts'.tr(), 5), + TextAndSwitch(LocaleKeys.settingPage_hotInfo.tr(), 5), const SizedBox(height: 10), ] ]; diff --git a/pubspec.lock b/pubspec.lock index 65df75ad..f23c30e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -310,6 +310,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_driver: dependency: "direct dev" description: flutter @@ -375,10 +383,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.1" flutter_local_notifications: dependency: "direct main" description: @@ -715,10 +723,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" logging: dependency: transitive description: @@ -1493,29 +1501,29 @@ packages: source: hosted version: "4.4.1" webview_flutter_android: - dependency: transitive + dependency: "direct main" description: name: webview_flutter_android - sha256: b0cd33dd7d3dd8e5f664e11a19e17ba12c352647269921a3b568406b001f1dff + sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0" url: "https://pub.dev" source: hosted - version: "3.12.0" + version: "3.15.0" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" + sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.9.0" webview_flutter_wkwebview: - dependency: transitive + dependency: "direct main" description: name: webview_flutter_wkwebview - sha256: "30b9af6bdd457b44c08748b9190d23208b5165357cc2eb57914fee1366c42974" + sha256: "4d062ad505390ecef1c4bfb6001cd857a51e00912cc9dfb66edb1886a9ebd80c" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "3.10.2" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b333aaee..0cd8ca69 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: new_ara_app description: A new Flutter project. publish_to: none -version: 1.0.2+102212113 +version: 1.1.0+103040304 environment: sdk: ">=2.18.0 <3.0.0" dependencies: @@ -41,10 +41,13 @@ dependencies: shared_preferences: "^2.2.2" file_icon: "^1.0.0" flutter_native_splash: "^2.3.7" + webview_flutter_android: "^3.15.0" + webview_flutter_wkwebview: "^3.10.2" + flutter_dotenv: "^5.1.0" dev_dependencies: flutter_test: sdk: flutter - flutter_lints: "^2.0.0" + flutter_lints: "^3.0.1" flutter_driver: sdk: flutter test: any @@ -54,18 +57,26 @@ flutter: - assets/translations/ - assets/images/ - assets/icons/ + - ".env.development" + - ".env.production" fonts: - - family: NotoSansKR + - family: Pretendard fonts: - - asset: assets/fonts/NotoSansKR-Black.otf + - asset: assets/fonts/Pretendard-Black.otf weight: 900 - - asset: assets/fonts/NotoSansKR-Bold.otf + - asset: assets/fonts/Pretendard-ExtraBold.otf + weight: 800 + - asset: assets/fonts/Pretendard-Bold.otf weight: 700 - - asset: assets/fonts/NotoSansKR-Medium.otf + - asset: assets/fonts/Pretendard-SemiBold.otf + weight: 600 + - asset: assets/fonts/Pretendard-Medium.otf weight: 500 - - asset: assets/fonts/NotoSansKR-Regular.otf + - asset: assets/fonts/Pretendard-Regular.otf weight: 400 - - asset: assets/fonts/NotoSansKR-Light.otf + - asset: assets/fonts/Pretendard-Light.otf weight: 300 - - asset: assets/fonts/NotoSansKR-Thin.otf + - asset: assets/fonts/Pretendard-ExtraLight.otf + weight: 200 + - asset: assets/fonts/Pretendard-Thin.otf weight: 100