Skip to content

Commit

Permalink
v1.0.0
Browse files Browse the repository at this point in the history
πŸš€ Feature Updates

- 앱이 Foreground에 μžˆμ–΄λ„ μ•Œλ¦Όμ΄ μ˜€κ²Œλ” 지원

πŸ”§ Bug Fixes

- λͺ©ν‘œ 가격 μ„€μ • λ²”μœ„κ°€ 0~999999999 μ‚¬μ΄μ—μ„œ μ„€μ •λ˜λ„λ‘ μˆ˜μ •
- μƒν’ˆ 상세 νŽ˜μ΄μ§€ 제λͺ© μ΅œλŒ€ 쀄 수λ₯Ό 3μ€„λ‘œ λ³€κ²½

Co-Authored-By: EunhoKang <[email protected]>
Co-Authored-By: ootr47 <[email protected]>
Co-Authored-By: 손문기 <[email protected]>
Co-Authored-By: ByeongIk Choi <[email protected]>
  • Loading branch information
5 people committed Dec 14, 2023
1 parent be7fe31 commit 88d9dc1
Show file tree
Hide file tree
Showing 14 changed files with 273 additions and 30 deletions.
126 changes: 122 additions & 4 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ android {
applicationId = "app.priceguard"
minSdk = 29
targetSdk = 34
versionCode = 6
versionName = "0.3.2"
versionCode = 7
versionName = "1.0.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,111 @@
package app.priceguard.service

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import app.priceguard.R
import app.priceguard.ui.detail.DetailActivity
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class PriceGuardFirebaseMessagingService : FirebaseMessagingService() {
// Init μ‹œμ—λ„ 호좜됨
override fun onNewToken(token: String) {
Log.d("PriceGuardFirebaseMessagingService", "Refreshed token: $token")
}

override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
message.notification?.let {
sendNotification(
it.title ?: return,
it.body ?: return,
it.imageUrl ?: return,
message.data["productCode"] ?: return
)
}
}

private fun sendNotification(title: String, body: String, imageUrl: Uri, data: String) {
val intent = Intent(this, DetailActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra("productCode", data)
intent.putExtra("directed", true)

val requestCode = getRequestCode()
val pendingIntent = PendingIntent.getActivity(
this,
requestCode,
intent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)

Glide.with(applicationContext)
.asBitmap()
.load(imageUrl)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
buildNotification(title, body, resource, pendingIntent, requestCode)
}

override fun onLoadCleared(placeholder: Drawable?) {}
})
}

private fun buildNotification(
title: String,
body: String,
image: Bitmap,
pendingIntent: PendingIntent,
requestCode: Int
) {
val channelId = getString(R.string.price_notification)
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_priceguard_notification)
.setContentTitle(title)
.setContentText(body)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setLargeIcon(image)
.setStyle(NotificationCompat.BigPictureStyle().bigPicture(image))
.setContentIntent(pendingIntent)

val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

val channel = NotificationChannel(
channelId,
getString(R.string.priceguard_push_alarm_title),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)

notificationManager.notify(requestCode, notificationBuilder.build())
}

companion object {
private var requestCode = 0

fun getRequestCode(): Int {
requestCode = (requestCode + 1) % 100
return requestCode
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,18 @@ class SetTargetPriceFragment : Fragment() {

private fun updateTargetPriceUI(it: Editable?) {
if (binding.etTargetPrice.isFocused) {
val targetPrice = if (it.toString().matches("^\\d+\$".toRegex())) {
it.toString().toFloat()
val targetPrice = if (it.toString().matches("^\\d{1,9}$".toRegex())) {
it.toString().toInt()
} else if (it.toString().isEmpty()) {
binding.etTargetPrice.setText(getString(R.string.min_price))
0
} else {
0F
binding.etTargetPrice.setText(getString(R.string.max_price))
999999999
}

setTargetPriceViewModel.updateTargetPrice(targetPrice.toInt())
binding.updateSlideValueWithPrice(targetPrice)
setTargetPriceViewModel.updateTargetPrice(targetPrice)
binding.updateSlideValueWithPrice(targetPrice.toFloat())
}
}

Expand Down Expand Up @@ -212,9 +216,8 @@ class SetTargetPriceFragment : Fragment() {

private fun FragmentSetTargetPriceBinding.updateSlideValueWithPrice(targetPrice: Float) {
val percent =
((targetPrice / setTargetPriceViewModel.state.value.productPrice) * MAX_PERCENT).toInt().roundAtFirstDigit()

val pricePercent = percent.coerceIn(MIN_PERCENT, MAX_PERCENT)
((targetPrice / setTargetPriceViewModel.state.value.productPrice) * MAX_PERCENT).toInt()
val pricePercent = percent.coerceIn(MIN_PERCENT, MAX_PERCENT).roundAtFirstDigit()
if (targetPrice > setTargetPriceViewModel.state.value.productPrice) {
tvTargetPricePercent.text = getString(R.string.over_current_price)
} else {
Expand Down
1 change: 0 additions & 1 deletion android/app/src/main/java/app/priceguard/ui/util/Dialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package app.priceguard.ui.util
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import app.priceguard.ui.ErrorDialogFragment
import app.priceguard.ui.data.DialogConfirmAction

fun AppCompatActivity.showConfirmDialog(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package app.priceguard.ui
package app.priceguard.ui.util

import android.app.Dialog
import android.content.Intent
Expand Down
2 changes: 1 addition & 1 deletion android/app/src/main/res/layout/activity_detail.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:singleLine="true"
android:maxLines="3"
android:text="@{viewModel.state.productName}"
android:textAppearance="?attr/textAppearanceTitleLarge"
app:layout_constraintEnd_toStartOf="@id/btn_detail_share"
Expand Down
3 changes: 3 additions & 0 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,7 @@
<string name="target_price_80">ν˜„μž¬ κ°€κ²©μ˜ 80%</string>
<string name="lowest_price_info">μ—­λŒ€ μ΅œμ €κ°€ %s</string>
<string name="target_price_info">λͺ©ν‘œ 가격 %s 으둜 섀정됨</string>
<string name="min_price">0</string>
<string name="max_price">999999999</string>
<string name="priceguard_push_alarm_title">Product Price Notification</string>
</resources>
15 changes: 9 additions & 6 deletions android/release_notes.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
December 12, 2023
v0.3.2
December 14, 2023
v1.0.0

πŸš€ Feature Updates

- 앱이 Foreground에 μžˆμ–΄λ„ μ•Œλ¦Όμ΄ μ˜€κ²Œλ” 지원


πŸ”§ Bug Fixes

- κ³΅μœ ν•˜κΈ°λ‘œ μƒν’ˆ 등둝 μ™„λ£Œ ν›„ 앱이 κΊΌμ§€λŠ” ν˜„μƒ μˆ˜μ •
- μƒν’ˆ 쀑볡 등둝 μ‹œ ν™ˆ ν™”λ©΄μœΌλ‘œ λŒμ•„κ°€λ„λ‘ μˆ˜μ •
- μƒν’ˆ μˆ˜μ • μ‹œ μƒμ„Έν™”λ©΄μœΌλ‘œ λŒμ•„κ°€λ„λ‘ μˆ˜μ •
- μ‹œμž‘ν™”λ©΄, 둜그인 ν™”λ©΄μ—μ„œ 글씨 크기 초과 μ‹œ ν…μŠ€νŠΈ 크기 μžλ™ μ‘°μ •
- λͺ©ν‘œ 가격 μ„€μ • λ²”μœ„κ°€ 0~999999999 μ‚¬μ΄μ—μ„œ μ„€μ •λ˜λ„λ‘ μˆ˜μ •
- μƒν’ˆ 상세 νŽ˜μ΄μ§€ 제λͺ© μ΅œλŒ€ 쀄 수λ₯Ό 3μ€„λ‘œ λ³€κ²½
4 changes: 4 additions & 0 deletions backend/src/cache/cache.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ export class CacheService {
return this.trackingProductCache.get(key);
}

getAllTrackingProduct() {
return this.trackingProductCache.getAll();
}

addValueTrackingProduct(key: string, value: TrackingProduct) {
this.trackingProductCache.addValue(key, value);
}
Expand Down
17 changes: 14 additions & 3 deletions backend/src/cache/tracking.cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class TrackingProductCache {
this.delete(key);
const node = new CacheNode(key, value);
this.add(node);
this.hashMap.set(key, node);
if (this.count > this.maxSize) {
const oldestNode = this.head.next;
this.remove(oldestNode);
Expand All @@ -32,22 +33,21 @@ export class TrackingProductCache {
node.next = this.tail;
node.prev = prev;
this.tail.prev = node;
this.hashMap.set(node.key, node);
this.count++;
}

delete(key: string) {
const node = this.hashMap.get(key);
if (node) {
this.remove(node);
this.hashMap.delete(key);
}
}

private remove(node: CacheNode<TrackingProduct[]>) {
const { prev, next } = node;
prev.next = next;
next.prev = prev;
this.hashMap.delete(node.key);
this.count--;
}

Expand All @@ -67,7 +67,12 @@ export class TrackingProductCache {

get(key: string): TrackingProduct[] | null {
const node = this.hashMap.get(key);
return node ? node.value : null;
if (node) {
this.remove(node);
this.add(node);
return node.value;
}
return null;
}

getAll() {
Expand All @@ -84,6 +89,8 @@ export class TrackingProductCache {
const node = this.hashMap.get(key);
if (node) {
node.value.push(value);
this.remove(node);
this.add(node);
}
}

Expand All @@ -93,6 +100,8 @@ export class TrackingProductCache {
node.value = node.value.filter((product) => {
return product.productId !== value.productId;
});
this.remove(node);
this.add(node);
}
}

Expand All @@ -105,6 +114,8 @@ export class TrackingProductCache {
return false;
}
});
this.remove(node);
this.add(node);
}
}

Expand Down
1 change: 1 addition & 0 deletions backend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ export const TWENTY_MIN_TO_SEC = 20 * 60;
export const TWO_WEEKS_TO_SEC = 2 * 7 * 24 * 60 * 60;
export const TWO_MONTHS_TO_SEC = 61 * 24 * 60 * 60;
export const MAX_TRACKING_PRODUCT_CACHE = parseInt(process.env.MAX_TRACKING_PRODUCT_CACHE || '30');
export const MAX_TARGET_PRICE = 999999999;
4 changes: 3 additions & 1 deletion backend/src/dto/product.add.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { IsNotEmpty, IsNumber, IsString, Max } from 'class-validator';
import { MAX_TARGET_PRICE } from 'src/constants';

export class ProductAddDto {
@ApiProperty({
Expand All @@ -16,6 +17,7 @@ export class ProductAddDto {
required: true,
})
@IsNumber()
@Max(MAX_TARGET_PRICE)
@IsNotEmpty()
targetPrice: number;
}
5 changes: 2 additions & 3 deletions backend/src/product/product.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { InjectModel } from '@nestjs/mongoose';
import { ProductPrice } from 'src/schema/product.schema';
import { Model } from 'mongoose';
import { PriceDataDto } from 'src/dto/price.data.dto';
import { MAX_TRACKING_RANK, NINETY_DAYS, THIRTY_DAYS, TWENTY_MIN_TO_SEC } from 'src/constants';
import { MAX_TRACKING_RANK, NINETY_DAYS, THIRTY_DAYS } from 'src/constants';
import { ProductRankCacheDto } from 'src/dto/product.rank.cache.dto';
import Redis from 'ioredis';
import { InjectRedis } from '@songkeys/nestjs-redis';
Expand Down Expand Up @@ -159,6 +159,7 @@ export class ProductService {
const product = await this.findTrackingProductByCode(userId, productAddDto.productCode);
product.targetPrice = productAddDto.targetPrice;
product.isFirst = true;
this.cacheService.updateValueTrackingProdcut(userId, product);
await this.trackingProductRepository.save(product);
}

Expand Down Expand Up @@ -264,8 +265,6 @@ export class ProductService {
isSoldOut: productInfo.isSoldOut,
lowestPrice: productInfo.productPrice,
}),
'EX',
TWENTY_MIN_TO_SEC,
);
this.productPriceModel.create(updatedDataInfo);
return product;
Expand Down

0 comments on commit 88d9dc1

Please sign in to comment.